슈팅게임 2
이번주는 저번주에 배운 내용 복습 겸 추가적인 기능 개발을 했다.
1. 조종 비행기를 앞, 뒤로 이동할 수 있게 기능 추가. (Player)
앞서 좌, 우로 이동할 수 있는 기능을 토대로 앞, 뒤로 이동할 수 있게 기능을 추가했다.
// 좌, 우 이동
float axis = Input.GetAxis("Horizontal");
Vector3 moveAmount = axis * Speed * -Vector3.right * Time.deltaTime;
myTransform.Translate(moveAmount);
// 앞, 뒤 이동
float axis2 = Input.GetAxis("Vertical");
Vector3 moveAmount2 = axis2 * Speed * -Vector3.forward * Time.deltaTime;
myTransform.Translate(moveAmount2);
위처럼 추가했는데 오브젝트의 로테이션 값에 따라 포지션의 방향? 이 달라지는 내용을 추가적으로 공부했다.
Player의 로테이션 x의 값이 90이었는데 0으로 수정하니 ↑ 또는 ↓ 키를 누르면 Player의 포지션 z 축 값만 변했다.
로테이션 x의 값이 0이 아닌 다른 값으로 변경하면 Player의 포지션 y축과 z축의 값이 같이 변하는 것 같다.
단순히 좌우상하가 변경되는 거 같은데...
2. 조종 비행기가 좌, 우로 이동할 때 맵을 벗어나지 않도록 기능 추가.
제목은 좌, 우 지만 하는 김에 상, 하도 했다. Unit스럽게 구현을 못했지만 주먹구구식으로 개발해 봤다.
(추후에 배워서 수정해야겠다...)
float horizAxis = Input.GetAxis("Horizontal");
if ((horizAxis > 0.0f && myTransform.position.x < 60.0f) || (horizAxis < 0.0f && myTransform.position.x > -60.0f)) {
Vector3 moveAmount = getMoveAmount(horizAxis, -Vector3.right);
myTransform.Translate(moveAmount);
}
float verticalAxis = Input.GetAxis("Vertical");
if ((verticalAxis > 0.0f && myTransform.position.y < 40.0f) || (verticalAxis < 0.0f && myTransform.position.y > -45.0f)) {
Vector3 moveAmount = getMoveAmount(verticalAxis, -Vector3.forward);
myTransform.Translate(moveAmount);
}
코드는 더럽지만 기능이 동작하도록 개발했다. 코드를 설명해 보면 Input.GetAxis 함수의 인자가 "Horizontal" 또는 "Vertical" 일 때 함수의 결과 값이 -1 ~ 1 사이의 실수인데 오브젝트가 좌, 우로 움직였을 때 가속도처럼 0에서 1 또는 -1까지 점진적으로 증가하는 값을 반환한다. 예를 들면 →키를 계속 누르고 있을 때 0 -> 0.1 -> 0.3 -> 0.5 -> 0.8 -> 1 이런 식으로 증가하는 값을 반환한다. 방향은 Vecter3.right, Vecter3.forward 와 같은 상수를 연산에 포함하면 결과 값으로 Vecter3을 반환하는데 Position의 x, y, z 축의 값이 저장돼 있다.
(update 함수가 일정 간격으로 계속 실행되면서 점진적으로 증가하는 거지 함수 자체에서 점진적으로 증가하는 값을 주는 게 아님)
getMoveAmount 함수는 클래스 내부에서만 접근할 수 있도록 만든 함수이다.
private Vector3 getMoveAmount(float axis, Vector3 direct) {
return axis * Speed * direct * Time.deltaTime;
}
3. 조종 비행기와 적 비행기가 부딪칠 때 LIFE가 줄어들도록 기능 추가.
강의에서 적 비행기와 미사일이 부딪칠 때 작동하는 방식을 적용할 거다.
적 비행기를 보면 초록색으로 테두리가 쳐 저 있는데 저 테두리가 물체와 물체가 부딪쳤다는 반응을 만들어주는 Box Collider라는 컴포넌트다. 물체와 물체가 부딪칠 때 물체끼리 받는 힘을 계산해서 적용해야 하는데 이런 복잡한 것들을 해주는 Rigidbody라는 컴포넌트다. 따라서 적 비행기와 조종 비행기(Player)가 충돌할 수 있도록 Player에게 Box Collider와 Rigidbody 컴포넌트를 추가한다.
두 컴포넌트를 추가해서 끝난 게 아니다. Box Collider의 Size를 설정해주지 않으면 X, Y, Z 크기만큼의 영역에 부딪쳐야 반응하기 때문에 Player에 크기에 맞게 Box Collider의 Size 값을 변경한다.
위와 같이 변경하게 되면 Player에 적 비행기와 같은 초록색 테두리가 생긴다.
이 상태에서 게임을 실행하면 적 비행기와 Player가 Rigidbody에 의해 서로 영향을 받는다.
위 움짤을 보면 Player가 뒤로 밀리는데 이러한 형상은 Rigidbody의 중력 옵션이 작동하기 때문인데 이 기능을 꺼야 Player와 적 비행기다 부딪칠 수 있다.
이제 스크립트로 Player와 적 비행기가 부딪쳤을 때의 발행하는 동작을 코딩하면 된다. Player 스크립트에 적 비행기와 부딪칠 때 반응하여 라이프를 1씩 깎도록 OnTriggerEnter 함수를 구현한다.
private void OnTriggerEnter(Collider other) {
if (other.tag == "Enemy") {
--MainControl.Life;
}
}
적 비행기와 부딪쳤을 때 LIFE는 깎이는데 이펙트도 없고 쫌 이상하긴 하지만 다음으로 넘어가야겠다.
4. LIFE가 0일 때 어떻게 처리할지 기획.
제목은 기획이지만 그냥 구현까지하면 좋을 거 같다.
Player의 LIFE가 0이 되면 그동안 획득했던 SCORE를 출력하고 다시 게임할 수 있는 화면으로 전환하면 좋을 거 같다.
MainControl 스크립트 파일의 update 함수에서 기능을 구현하면 될 거 같다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainControl : MonoBehaviour {
static public int Score = 0;
static public int Life = 3;
public GUISkin mySkin = null;
// Update is called once per frame
private void OnGUI() {
GUI.skin = mySkin;
Rect labelRect1 = new Rect (10.0f, 10.0f, 400.0f, 100.0f); // 위치 x, 위치 y, 폭, 높이
GUI.Label(labelRect1, "SCORE: " + MainControl.Score);
Rect labelRect2 = new Rect (10.0f, 110.0f, 400.0f, 100.0f); // 위치 x, 위치 y, 폭, 높이
GUI.Label(labelRect2, "LIFE: " + MainControl.Life);
}
void Update () {
if (MainControl.Score > 500) {
MainControl.Score = 0;
UnityEngine.SceneManagement.SceneManager.LoadScene("Victory");
}
// LIFE가 0일 때 처리.
}
}
이제 Scene를 만들고 화면을 전환해 보자.
Result Scene을 만들고 empty Object를 만든 후 스크립트 컴포넌트를 추가한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameResultControl : MonoBehaviour {
private void OnGUI() {
float CenterX = Screen.width * 0.5f;
float CenterY = Screen.height * 0.5f;
GUI.skin.label.fontSize = 45;
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
Rect labelRect1 = new Rect (CenterX - 245.0f, CenterY - 460.0f, 500.0f, 600.0f); // 위치 x, 위치 y, 폭, 높이
GUI.Label(labelRect1, "GAME OVER!");
GUI.skin.label.fontSize = 30;
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
Rect labelRect2 = new Rect (CenterX - 245.0f, CenterY - 415.0f, 500.0f, 600.0f); // 위치 x, 위치 y, 폭, 높이
GUI.Label(labelRect2, "YOUR SCORE");
Rect labelRect3 = new Rect (CenterX - 245.0f, CenterY - 385.0f, 500.0f, 600.0f); // 위치 x, 위치 y, 폭, 높이
GUI.Label(labelRect3, "" + MainControl.Score);
Rect ButtonRect = new Rect(CenterX - 100.0f, CenterY - 60.0f, 200.0f, 200.0f);
if(GUI.Button(ButtonRect, "Game Start!!") == true) {
SceneManager.LoadScene("Level1");
MainControl.Life = 3;
MainControl.Score = 0;
}
}
}
스크립트 내용을 위와 같다. update와 start 함수가 없는데 update 함수를 지워야 필요 없는 자원을 소비하지 않는다 한다.
그리고 게임 실행해서 테스트하다 오류가 발생했는데
Result Scene을 못 찾은 거 같다. Result Scene을 빌드할 때 추가해 보자.
위 이미지의 빌드 세팅 창에서
이렇게 Scene 파일을 드래그해서 추가해 주면 된다.
동영상을 재생해 보면 잘 동작한다.
5. 스코어에 따라 적 비행기를 추가되는 기능 추가.
이게 이번주에 제일 고민이 되는 기능이었는데 일단 개발 후 나중에 더 괜찮은 비슷한 기능을 배우면 수정하도록 하겠다.
작업 순서는
- MainControl 스크립트에서 Score가 500보다 크면 Victory Scene으로 전환되는 코드 제거
- 적 비행기를 생성하는 Object를 만들기 (EnemyFactoryControl)
- Score에 따라 Level을 증가할 수 있도록 MainControl 스크립트에 기능 추가.
- EnemyFactoryControl에서 Level에 따라 적 비행기를 생성
이렇게 하면 될 거 같은데 차근차근해보자.
일단 MainControl 스크립트에서 Score가 500보다 크면 Victory Scene으로 전환되는 코드부터 제거한다.
void Update () {
// if (MainControl.Score > 500) {
// MainControl.Score = 0;
// UnityEngine.SceneManagement.SceneManager.LoadScene("Victory");
// }
// LIFE가 0일 때 처리.
if (MainControl.Life == 0) {
UnityEngine.SceneManagement.SceneManager.LoadScene("Result");
}
}
EnemyFactoryControl Object와 스크립트 추가
다음으론 EnemyFactoryControl 스크립트를 코딩
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyFactoryControl : MonoBehaviour {
public int[] enemyGenInfo = null;
public GameObject enemy = null;
public static int enemyCnt = 0;
// Update is called once per frame
void Update () {
int maxEnemyCnt = enemyGenInfo[0];
if (enemyCnt < maxEnemyCnt) {
Quaternion rotation = Quaternion.Euler(-90.0f, 0, 0);
Instantiate(enemy, getPosition(), rotation);
++enemyCnt;
}
}
private Vector3 getPosition() {
return new Vector3(Random.Range(-60.0f, 60.0f), 55.0f, 0);
}
}
enemyGenInfo 배열에는 레벨별로 적 비행기를 생성할 숫자를 저장하고 enemyCnt는 화면에 생성된 적 비행기의 숫자다.
적 비행기를 복제할 때 Rotation의 x축을 -90으로 만들어야 하기 때문에 Quaternion을 사용해 설정하고 Position의 x 축은 좌, 우를 나타내는 값이라 -60 ~ 60까지 범위의 랜덤 값으로 설정하고 y축을 55로 고정하여 화면 맨 끝 위치에서 나오도록 설정한다.
다음은 적 비행기 스크립트다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyControl : MonoBehaviour {
public float EnemySpeed = 15.0f;
public GameObject Explosion = null;
private Transform myTransform = null;
// Use this for initialization
void Start () {
myTransform = GetComponent<Transform>();
}
// Update is called once per frame
void Update () {
Vector3 moveAmount = EnemySpeed * Vector3.back * Time.deltaTime;
myTransform.Translate(moveAmount);
if (myTransform.position.y < -50.0f) {
InitPosition();
}
}
void InitPosition() {
myTransform.position = new Vector3(Random.Range(-60.0f, 60.0f), 55.0f, 0);
}
private void OnTriggerEnter(Collider other) {
if (other.tag == "Bullet") {
MainControl.Score += 100;
Instantiate(Explosion, new Vector3(myTransform.position.x, myTransform.position.y, 0), Quaternion.identity);
InitPosition();
Destroy(other.gameObject);
}
}
}
Update 함수에서 위에서 아래로 이동할 수 있는 기능이 구현돼 있는데 적 비행기의 Position y축이 -50 보다 작으면 다시 위로 올리고 좌, 우 위치를 수정한다.
처음엔 Position y축이 -50 보다 작으면 적 비행기를 삭제했는데 굳이 그럴 필요가 없을 것 같다. OnTriggerEnter 함수에서도 똑같은 이유로 삭제보다는 위치를 이동하게 만들었다.
잘 동작하는 것 같으니 이제 MainControl 스크립트 파일에서 Score 별로 Level을 올려보자.
static public int Level = 1;
private Dictionary<int, int> levelInfo = new Dictionary<int, int>();
void Start() {
levelInfo.Add(1, 500);
levelInfo.Add(2, 1200);
levelInfo.Add(3, 2000);
levelInfo.Add(4, 3000);
levelInfo.Add(5, 4500);
levelInfo.Add(6, 5300);
levelInfo.Add(7, 8000);
levelInfo.Add(8, 12000);
levelInfo.Add(9, 190000);
levelInfo.Add(10, 270000);
}
이렇게 Level별 Level up 하기 위한 Score를 Map으로 만들어 놓고
void Update () {
if (levelInfo.ContainsKey(MainControl.Level)) {
int levelUpScore = levelInfo[MainControl.Level];
if (levelUpScore <= MainControl.Score) {
++MainControl.Level;
}
}
// LIFE가 0일 때 처리.
if (MainControl.Life == 0) {
UnityEngine.SceneManagement.SceneManager.LoadScene("Result");
}
}
Update 함수에서 Score에 따라서 Level 변수를 1씩 올려준다.
이제 결과를 보면
Level 7 이상되면 게임이 터지는데 그 이유는 EnemyFactoryControl 스크립트의 enemyGenInfo 때문인데 Level 7 이상일 땐 enemy의 속도를 증가시켜 보겠다.
void Update () {
if (MainControl.Level - 1 < enemyGenInfo.Length) {
int maxEnemyCnt = enemyGenInfo [MainControl.Level - 1];
if (enemyCnt < maxEnemyCnt) {
Quaternion rotation = Quaternion.Euler (-90.0f, 0, 0);
GameObject clone = Instantiate (enemy, getPosition (), rotation);
enemyList.Add(clone);
++enemyCnt;
oldLevel = MainControl.Level;
}
} else if (oldLevel != MainControl.Level) {
foreach(GameObject enemy in enemyList) {
enemy.GetComponent<EnemyControl>().EnemySpeed += 5.0f;
}
oldLevel = MainControl.Level;
}
}
이렇게 수정했는데 동작은 하니... 패스....
그리고 Level이 11이 되면 Victory Scene으로 이동시킨다.
결과
다음주엔 지금까지 작업했던 내용으로 나만의 게임을 만들어 볼 예정이다.
1. 땅땅 마법사 같은 게임 만들어보기.
'Unity > fastcampus' 카테고리의 다른 글
2024.12.30 ~ 2025.01.05 (0) | 2025.01.05 |
---|