기록 보관소

[Unity/유니티] 기초-2D 종스크롤 슈팅: 모바일 슈팅게임 만들기[BE4] 본문

유니티 프로젝트/2D 종스크롤 슈팅

[Unity/유니티] 기초-2D 종스크롤 슈팅: 모바일 슈팅게임 만들기[BE4]

JongHoon 2022. 3. 12. 23:38

개요

유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.

현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

유니티 강좌 기초 채널 Basic

유니티 개발을 처음 시작하시는 입문자 분들을 위한 기초 채널. [ 프로젝트 ] B00 ~ B12 (BE1) : 유니티 필수 기초 B13 ~ B19 (BE2) : 2D 플랫포머 B20 ~ B26 (BE3) : 2D 탑다운 대화형 RPG B27 ~ B37 (BE4) : 2D 종스크롤

www.youtube.com


2D 종스크롤 슈팅: 모바일 슈팅게임 만들기[BE4]

1. 플레이어 무적 시간

//Player 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {
    public GameManager gameManager;
    public ObjectManager objectManager;
    public GameObject bulletObjA;
    public GameObject bulletObjB;
    public GameObject boomEffect;
    public GameObject[] followers;
    Animator anim;
    SpriteRenderer spriteRenderer;

    public int life;
    public int score;
    public float speed;
    public int maxPower;
    public int power;
    public int maxBoom;
    public int boom;
    public float maxShotDelay;
    public float curShotDelay;

    public bool isTouchTop;
    public bool isTouchBottom;
    public bool isTouchLeft;
    public bool isTouchRight;
    public bool isHit;
    public bool isBoomTime;

    public bool isRespawnTime;

    void Awake() {
        anim = GetComponent<Animator>();
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    void OnEnable() {
        Unbeatable();
        Invoke("Unbeatable", 3);
    }

    void Unbeatable() {
        isRespawnTime = !isRespawnTime; //값을 반대로

        if (isRespawnTime) {    //무적 시간 (반투명화)
            isRespawnTime = true;
            spriteRenderer.color = new Color(1, 1, 1, 0.5f);    //플레이어 스프라이트

            for (int index = 0; index < followers.Length; index++) {    //팔로워 스프라이트
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0.5f);
            }
        }
        else {  //무적 시간 종료 (원래대로)
            isRespawnTime = false;
            spriteRenderer.color = new Color(1, 1, 1, 1);

            for (int index = 0; index < followers.Length; index++) {
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 1);
            }
        }
    }

    void Update() {
        Move();
        Fire();
        Boom();
        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 curPos = transform.position;
        Vector3 nextPos = new Vector3(h, v, 0) * speed * Time.deltaTime;

        transform.position = curPos + nextPos;

        if (Input.GetButtonDown("Horizontal") || Input.GetButtonUp("Horizontal"))
            anim.SetInteger("Input", (int)h);
    }

    void Fire() {   //플레이어 총알 발사 함수
        if (!Input.GetButton("Fire1"))  //Ctrl 키, 마우스 좌클릭
            return;

        if (curShotDelay < maxShotDelay)    //장전 중이라면
            return;

        switch(power) {
            case 1:
                GameObject bullet = objectManager.MakeObj("BulletPlayerA");
                bullet.transform.position = transform.position;
                Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
                rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 2:
                GameObject bulletR = objectManager.MakeObj("BulletPlayerA");
                bulletR.transform.position = transform.position + Vector3.right * 0.1f;
                GameObject bulletL = objectManager.MakeObj("BulletPlayerA");
                bulletL.transform.position = transform.position + Vector3.left * 0.1f;
                Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
                rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            default:
                GameObject bulletRR = objectManager.MakeObj("BulletPlayerA");
                bulletRR.transform.position = transform.position + Vector3.right * 0.35f;
                GameObject bulletCC = objectManager.MakeObj("BulletPlayerB");
                bulletCC.transform.position = transform.position;
                GameObject bulletLL = objectManager.MakeObj("BulletPlayerA");
                bulletLL.transform.position = transform.position + Vector3.left * 0.35f;
                Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidCC = bulletCC.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();
                rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
        }
        
        curShotDelay = 0;
    }

    void Reload() {
        curShotDelay += Time.deltaTime;
    }

    void Boom() {
        if (!Input.GetButton("Fire2"))  //Alt 키, 마우스 우클릭
            return;

        if (isBoomTime) //Boom 재사용 대기시간 중이라면
            return;

        if (boom == 0)  //필살기가 없다면
            return;

        boom--;
        isBoomTime = true;
        gameManager.UpdateBoomIcon(boom);

        //Boom 활성화
        boomEffect.SetActive(true);
        Invoke("OffBoomEffect", 4f);

        //모든 적 처치
        GameObject[] enemiesL = objectManager.GetPool("EnemyL");
        GameObject[] enemiesM = objectManager.GetPool("EnemyM");
        GameObject[] enemiesS = objectManager.GetPool("EnemyS");
        for (int index = 0; index < enemiesL.Length; index++)
            if (enemiesL[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesL[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesM.Length; index++)
            if (enemiesM[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesM[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesS.Length; index++)
            if (enemiesS[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesS[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }

        //모든 적 총알 제거
        GameObject[] bulletsA = objectManager.GetPool("BulletEnemyA");
        GameObject[] bulletsB = objectManager.GetPool("BulletEnemyB");
        for (int index = 0; index < bulletsA.Length; index++)
            if (bulletsA[index].activeSelf)     //활성화된 총알만 제거
                bulletsA[index].SetActive(false);
        for (int index = 0; index < bulletsB.Length; index++)
            if (bulletsB[index].activeSelf)     //활성화된 총알만 제거
                bulletsB[index].SetActive(false);
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch(collision.gameObject.name) {
                case "Top":
                    isTouchTop = true;
                    break;
                case "Bottom":
                    isTouchBottom = true;
                    break;
                case "Left":
                    isTouchLeft = true;
                    break;
                case "Right":
                    isTouchRight = true;
                    break;
            }
        }
        else if (collision.gameObject.tag == "Enemy" || collision.gameObject.tag == "EnemyBullet") {
            if (isRespawnTime)  //무적 시간일때
                return;

            if (isHit)  //이미 맞으면 다시 맞지 않도록 return
                return;

            isHit = true;
            life--;
            gameManager.UpdateLifeIcon(life);

            if (life == 0) {
                gameManager.GameOver();
            }
            else {
                gameManager.RespawnPlayer();
            }
            gameObject.SetActive(false);
            if (collision.gameObject.tag == "Enemy") {
                Enemy enemyObject = collision.gameObject.GetComponent<Enemy>();
                if (enemyObject.enemyName == "B")
                    return;
                else
                    collision.gameObject.SetActive(false);
            }
        }
        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++;
                        AddFollower();
                    }
                    break;
                case "Boom":
                    if (boom == maxBoom)
                        score += 500;
                    else {
                        boom++;
                        gameManager.UpdateBoomIcon(boom);
                    }
                    break;
            }

            collision.gameObject.SetActive(false);
        }
    }

    void OffBoomEffect() {
        boomEffect.SetActive(false);
        isBoomTime = false;
    }

    void AddFollower() {
        if (power == 4)
            followers[0].SetActive(true);
        else if (power == 5)
            followers[1].SetActive(true);
        else if (power == 6)
            followers[2].SetActive(true);

    }

    void OnTriggerExit2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch (collision.gameObject.name) {
                case "Top":
                    isTouchTop = false;
                    break;
                case "Bottom":
                    isTouchBottom = false;
                    break;
                case "Left":
                    isTouchLeft = false;
                    break;
                case "Right":
                    isTouchRight = false;
                    break;
            }
        }
    }
}

Player 자식 오브젝트로 추가
맞기 직전
맞은 후
반투명화 상태로 무적이 되었다

  • 보조무기 Follower들을 플레이어 자식 오브젝트로 넣어서 플레이어가 죽으면 같이 비활성화되도록 만들었다.

2. 폭발 효과

폭발 스프라이트 Explosion 생성
Explosion 애니메이션 생성
Explosion의 Animator창에 Empty 생성
Idle을 default로 잡아준다
Trigger형 매개변수 OnExplosion 생성 및&nbsp;Transition 설정&nbsp;

  • Explosion -> Idle은 Transition Duration만 0으로 수정해준다.

Explosion 스크립트 생성

//Explosion 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Explosion : MonoBehaviour {
    Animator anim;

    void Awake() {
        anim = GetComponent<Animator>();
    }

    void OnEnable() {
        Invoke("Disable", 2f);
    }

    void Disable() {
        gameObject.SetActive(false);
    }

    public void startExplosion(string target) {
        anim.SetTrigger("OnExplosion");

        switch (target) {   //타겟에 따른 폭발 크기 변경
            case "S":
                transform.localScale = Vector3.one * 0.7f;
                break;
            case "P":
            case "M":
                transform.localScale = Vector3.one * 1f;
                break;
            case "L":
                transform.localScale = Vector3.one * 2f;
                break;
            case "B":
                transform.localScale = Vector3.one * 3f;
                break;
        }
    }
}

Explosion에 넣어주고
Explosion을 프리펩으로 만들어준다
프리펩 스프라이트는 None으로 설정

//ObjectManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectManager : MonoBehaviour {
    public GameObject enemyBPrefab;
    public GameObject enemyLPrefab;
    public GameObject enemyMPrefab;
    public GameObject enemySPrefab;

    public GameObject itemCoinPrefab;
    public GameObject itemPowerPrefab;
    public GameObject itemBoomPrefab;

    public GameObject bulletPlayerAPrefab;
    public GameObject bulletPlayerBPrefab;
    public GameObject bulletEnemyAPrefab;
    public GameObject bulletEnemyBPrefab;
    public GameObject bulletFollowerPrefab;
    public GameObject bulletBossAPrefab;
    public GameObject bulletBossBPrefab;

    public GameObject explosionPrefab;

    GameObject[] enemyB;
    GameObject[] enemyL;
    GameObject[] enemyM;
    GameObject[] enemyS;

    GameObject[] itemCoin;
    GameObject[] itemPower;
    GameObject[] itemBoom;

    GameObject[] bulletPlayerA;
    GameObject[] bulletPlayerB;
    GameObject[] bulletEnemyA;
    GameObject[] bulletEnemyB;
    GameObject[] bulletFollower;
    GameObject[] bulletBossA;
    GameObject[] bulletBossB;

    GameObject[] explosion;

    GameObject[] targetPool;

    void Awake() {
        enemyB = new GameObject[1];
        enemyL = new GameObject[10];
        enemyM = new GameObject[10];
        enemyS = new GameObject[20];

        itemCoin = new GameObject[20];
        itemPower = new GameObject[10];
        itemBoom = new GameObject[10];

        bulletPlayerA = new GameObject[100];
        bulletPlayerB = new GameObject[100];
        bulletEnemyA = new GameObject[100];
        bulletEnemyB = new GameObject[100];
        bulletFollower = new GameObject[100];
        bulletBossA = new GameObject[50];
        bulletBossB = new GameObject[1000];
        explosion = new GameObject[20];

        Generate();
    }

    void Generate() {
        //적 기체
        for (int index = 0; index < enemyB.Length; index++) {
            enemyB[index] = Instantiate(enemyBPrefab);
            enemyB[index].SetActive(false);
        }
        for (int index = 0; index < enemyL.Length; index++) {
            enemyL[index] = Instantiate(enemyLPrefab);
            enemyL[index].SetActive(false);
        }
        for (int index = 0; index < enemyM.Length; index++) {
            enemyM[index] = Instantiate(enemyMPrefab);
            enemyM[index].SetActive(false);
        }
        for (int index = 0; index < enemyS.Length; index++) {
            enemyS[index] = Instantiate(enemySPrefab);
            enemyS[index].SetActive(false);
        }
        
        //아이템
        for (int index = 0; index < itemCoin.Length; index++) {
            itemCoin[index] = Instantiate(itemCoinPrefab);
            itemCoin[index].SetActive(false);
        }
        for (int index = 0; index < itemPower.Length; index++) {
            itemPower[index] = Instantiate(itemPowerPrefab);
            itemPower[index].SetActive(false);
        }
        for (int index = 0; index < itemBoom.Length; index++) {
            itemBoom[index] = Instantiate(itemBoomPrefab);
            itemBoom[index].SetActive(false);
        }

        //총알
        for (int index = 0; index < bulletPlayerA.Length; index++) {
            bulletPlayerA[index] = Instantiate(bulletPlayerAPrefab);
            bulletPlayerA[index].SetActive(false);
        }
        for (int index = 0; index < bulletPlayerB.Length; index++) {
            bulletPlayerB[index] = Instantiate(bulletPlayerBPrefab);
            bulletPlayerB[index].SetActive(false);
        }
        for (int index = 0; index < bulletEnemyA.Length; index++) {
            bulletEnemyA[index] = Instantiate(bulletEnemyAPrefab);
            bulletEnemyA[index].SetActive(false);
        }
        for (int index = 0; index < bulletEnemyB.Length; index++) {
            bulletEnemyB[index] = Instantiate(bulletEnemyBPrefab);
            bulletEnemyB[index].SetActive(false);
        }
        for (int index = 0; index < bulletFollower.Length; index++) {
            bulletFollower[index] = Instantiate(bulletFollowerPrefab);
            bulletFollower[index].SetActive(false);
        }
        for (int index = 0; index < bulletBossA.Length; index++) {
            bulletBossA[index] = Instantiate(bulletBossAPrefab);
            bulletBossA[index].SetActive(false);
        }
        for (int index = 0; index < bulletBossB.Length; index++) {
            bulletBossB[index] = Instantiate(bulletBossBPrefab);
            bulletBossB[index].SetActive(false);
        }

        //폭발
        for (int index = 0; index < explosion.Length; index++) {
            explosion[index] = Instantiate(explosionPrefab);
            explosion[index].SetActive(false);
        }
    }

    public GameObject MakeObj(string type) {
        switch (type) {
            case "EnemyB":
                targetPool = enemyB;
                break;
            case "EnemyL":
                targetPool = enemyL;
                break;
            case "EnemyM":
                targetPool = enemyM;
                break;
            case "EnemyS":
                targetPool = enemyS;
                break;
            case "ItemCoin":
                targetPool = itemCoin;
                break;
            case "ItemPower":
                targetPool = itemPower;
                break;
            case "ItemBoom":
                targetPool = itemBoom;
                break;
            case "BulletPlayerA":
                targetPool = bulletPlayerA;
                break;
            case "BulletPlayerB":
                targetPool = bulletPlayerB;
                break;
            case "BulletEnemyA":
                targetPool = bulletEnemyA;
                break;
            case "BulletEnemyB":
                targetPool = bulletEnemyB;
                break;
            case "BulletFollower":
                targetPool = bulletFollower;
                break;
            case "BulletBossA":
                targetPool = bulletBossA;
                break;
            case "BulletBossB":
                targetPool = bulletBossB;
                break;
            case "Explosion":
                targetPool = explosion;
                break;
        }

        for (int index = 0; index < targetPool.Length; index++) {
            if (!targetPool[index].activeSelf) {    //비활성화된 오브젝트에 접근
                targetPool[index].SetActive(true);  //해당 오브젝트를 활성화 후
                return targetPool[index];   //오브젝트 반환
            }
        }

        return null;
    }

    public GameObject[] GetPool(string type) {
        switch (type) {
            case "EnemyB":
                targetPool = enemyB;
                break;
            case "EnemyL":
                targetPool = enemyL;
                break;
            case "EnemyM":
                targetPool = enemyM;
                break;
            case "EnemyS":
                targetPool = enemyS;
                break;
            case "ItemCoin":
                targetPool = itemCoin;
                break;
            case "ItemPower":
                targetPool = itemPower;
                break;
            case "ItemBoom":
                targetPool = itemBoom;
                break;
            case "BulletPlayerA":
                targetPool = bulletPlayerA;
                break;
            case "BulletPlayerB":
                targetPool = bulletPlayerB;
                break;
            case "BulletEnemyA":
                targetPool = bulletEnemyA;
                break;
            case "BulletEnemyB":
                targetPool = bulletEnemyB;
                break;
            case "BulletFollower":
                targetPool = bulletFollower;
                break;
            case "BulletBossA":
                targetPool = bulletBossA;
                break;
            case "BulletBossB":
                targetPool = bulletBossB;
                break;
            case "Explosion":
                targetPool = explosion;
                break;
        }

        return targetPool;
    }
}

 

//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.IO;    //파일 읽기용 라이브러리. C#에서 지원

public class GameManager : MonoBehaviour {
    string[] enemyObjs;
    public Transform[] spawnPoints;
    public GameObject player;
    public Text scoreText;
    public Image[] lifeImage;
    public Image[] boomImage;
    public GameObject gameOverSet;
    public ObjectManager objectManager;

    public float nextSpawnDelay;
    public float curSpawnDelay;

    public List<Spawn> spawnList;
    public int spawnIndex;
    public bool spawnEnd;

    void Awake() {
        enemyObjs = new string[] { "EnemyS", "EnemyM", "EnemyL", "EnemyB" };
        spawnList = new List<Spawn>();
        ReadSpawnFile();
    }

    void ReadSpawnFile() {
        //변수 초기화
        spawnList.Clear();
        spawnIndex = 0;
        spawnEnd = false;

        //텍스트 파일 읽기
        TextAsset textFile = Resources.Load("Stage 0") as TextAsset;    //텍스트 파일 Stage 0 불러오기
        StringReader stringReader = new StringReader(textFile.text);

        while (stringReader != null) {
            string line = stringReader.ReadLine();

            if (line == null)
                break;

            //스폰 데이터 생성
            Spawn spawnData = new Spawn();
            spawnData.delay = float.Parse(line.Split(',')[0]);
            spawnData.type = line.Split(',')[1];
            spawnData.point = int.Parse(line.Split(',')[2]);
            spawnList.Add(spawnData);
        }

        //텍스트 파일 닫기
        stringReader.Close();

        //첫번째 스폰 딜레이 적용
        nextSpawnDelay = spawnList[0].delay;
    }

    void Update() {
        curSpawnDelay += Time.deltaTime;

        if (curSpawnDelay > nextSpawnDelay && !spawnEnd) {
            SpawnEnemy();
            curSpawnDelay = 0;
        }

        //UI 점수 업데이트
        Player playerLogic = player.GetComponent<Player>();
        scoreText.text = string.Format("{0:n0}", playerLogic.score);
    }

    void SpawnEnemy() {
        int enemyIndex = 0;
        switch(spawnList[spawnIndex].type) {
            case "S":
                enemyIndex = 0;
                break;
            case "M":
                enemyIndex = 1;
                break;
            case "L":
                enemyIndex = 2;
                break;
            case "B":
                enemyIndex = 3;
                break;
        }
        int enemyPoint = spawnList[spawnIndex].point;
        GameObject enemy = objectManager.MakeObj(enemyObjs[enemyIndex]);
        enemy.transform.position = spawnPoints[enemyPoint].position;
        Rigidbody2D rigid = enemy.GetComponent<Rigidbody2D>();
        Enemy enemyLogic = enemy.GetComponent<Enemy>();
        enemyLogic.player = player; //플레이어 클래스 넘겨주기
        enemyLogic.gameManager = this;  //클래스 자신을 넘겨주기
        enemyLogic.objectManager = objectManager;   //오브젝트 매니저 클래스 넘겨주기

        if (enemyPoint == 5 || enemyPoint == 6) {   //오른쪽 포인트
            enemy.transform.Rotate(Vector3.back * 90);  //적 기체 스프라이트 돌리기
            rigid.velocity = new Vector2(enemyLogic.speed * (-1), -1);
        }
        else if (enemyPoint == 7 || enemyPoint == 8) {  //왼쪽 포인트
            enemy.transform.Rotate(Vector3.back * (-90));  //적 기체 스프라이트 돌리기
            rigid.velocity = new Vector2(enemyLogic.speed, -1);
        }
        else {  //중앙 앞쪽 포인트
            rigid.velocity = new Vector2(0, enemyLogic.speed * (-1));
        }

        //스폰 인덱스 증가
        spawnIndex++;
        if (spawnIndex == spawnList.Count) {
            spawnEnd = true;
            return;
        }

        //다음 리스폰 딜레이 갱신
        nextSpawnDelay = spawnList[spawnIndex].delay;
    }

    public void UpdateLifeIcon(int life) {
        //UI 라이프 모두 투명화
        for (int index = 0; index < 3; index++) {
            lifeImage[index].color = new Color(1, 1, 1, 0);
        }

        //UI 남은 라이프 수만큼 활성화
        for (int index = 0; index < life; index++) {
            lifeImage[index].color = new Color(1, 1, 1, 1);
        }
    }

    public void UpdateBoomIcon(int boom) {
        //UI 붐 모두 투명화
        for (int index = 0; index < 3; index++) {
            boomImage[index].color = new Color(1, 1, 1, 0);
        }

        //UI 남은 붐 수만큼 활성화
        for (int index = 0; index < boom; index++) {
            boomImage[index].color = new Color(1, 1, 1, 1);
        }
    }

    public void RespawnPlayer() {
        Invoke("RespawnPlayerExe", 2f);
    }

    void RespawnPlayerExe() {
        player.transform.position = Vector3.down * 3.5f;    //플레이어 위치 초기화
        player.SetActive(true);

        Player playerLogic = player.GetComponent<Player>();
        playerLogic.isHit = false;
    }

    public void CallExplosion(Vector3 pos, string type) {
        GameObject explosion = objectManager.MakeObj("Explosion");
        Explosion explosionLogic = explosion.GetComponent<Explosion>();

        explosion.transform.position = pos; //위치 설정
        explosionLogic.startExplosion(type);    //크기 설정
    }

    public void GameOver() {
        gameOverSet.SetActive(true);
    }

    public void GameRetry() {
        SceneManager.LoadScene(0);
    }
}

 

//Player 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {
    public GameManager gameManager;
    public ObjectManager objectManager;
    public GameObject bulletObjA;
    public GameObject bulletObjB;
    public GameObject boomEffect;
    public GameObject[] followers;
    Animator anim;
    SpriteRenderer spriteRenderer;

    public int life;
    public int score;
    public float speed;
    public int maxPower;
    public int power;
    public int maxBoom;
    public int boom;
    public float maxShotDelay;
    public float curShotDelay;

    public bool isTouchTop;
    public bool isTouchBottom;
    public bool isTouchLeft;
    public bool isTouchRight;
    public bool isHit;
    public bool isBoomTime;

    public bool isRespawnTime;

    void Awake() {
        anim = GetComponent<Animator>();
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    void OnEnable() {
        Unbeatable();
        Invoke("Unbeatable", 3);
    }

    void Unbeatable() {
        isRespawnTime = !isRespawnTime; //값을 반대로

        if (isRespawnTime) {    //무적 시간 (반투명화)
            isRespawnTime = true;
            spriteRenderer.color = new Color(1, 1, 1, 0.5f);    //플레이어 스프라이트

            for (int index = 0; index < followers.Length; index++) {    //팔로워 스프라이트
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0.5f);
            }
        }
        else {  //무적 시간 종료 (원래대로)
            isRespawnTime = false;
            spriteRenderer.color = new Color(1, 1, 1, 1);

            for (int index = 0; index < followers.Length; index++) {
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 1);
            }
        }
    }

    void Update() {
        Move();
        Fire();
        Boom();
        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 curPos = transform.position;
        Vector3 nextPos = new Vector3(h, v, 0) * speed * Time.deltaTime;

        transform.position = curPos + nextPos;

        if (Input.GetButtonDown("Horizontal") || Input.GetButtonUp("Horizontal"))
            anim.SetInteger("Input", (int)h);
    }

    void Fire() {   //플레이어 총알 발사 함수
        if (!Input.GetButton("Fire1"))  //Ctrl 키, 마우스 좌클릭
            return;

        if (curShotDelay < maxShotDelay)    //장전 중이라면
            return;

        switch(power) {
            case 1:
                GameObject bullet = objectManager.MakeObj("BulletPlayerA");
                bullet.transform.position = transform.position;
                Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
                rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 2:
                GameObject bulletR = objectManager.MakeObj("BulletPlayerA");
                bulletR.transform.position = transform.position + Vector3.right * 0.1f;
                GameObject bulletL = objectManager.MakeObj("BulletPlayerA");
                bulletL.transform.position = transform.position + Vector3.left * 0.1f;
                Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
                rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            default:
                GameObject bulletRR = objectManager.MakeObj("BulletPlayerA");
                bulletRR.transform.position = transform.position + Vector3.right * 0.35f;
                GameObject bulletCC = objectManager.MakeObj("BulletPlayerB");
                bulletCC.transform.position = transform.position;
                GameObject bulletLL = objectManager.MakeObj("BulletPlayerA");
                bulletLL.transform.position = transform.position + Vector3.left * 0.35f;
                Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidCC = bulletCC.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();
                rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
        }
        
        curShotDelay = 0;
    }

    void Reload() {
        curShotDelay += Time.deltaTime;
    }

    void Boom() {
        if (!Input.GetButton("Fire2"))  //Alt 키, 마우스 우클릭
            return;

        if (isBoomTime) //Boom 재사용 대기시간 중이라면
            return;

        if (boom == 0)  //필살기가 없다면
            return;

        boom--;
        isBoomTime = true;
        gameManager.UpdateBoomIcon(boom);

        //Boom 활성화
        boomEffect.SetActive(true);
        Invoke("OffBoomEffect", 4f);

        //모든 적 처치
        GameObject[] enemiesL = objectManager.GetPool("EnemyL");
        GameObject[] enemiesM = objectManager.GetPool("EnemyM");
        GameObject[] enemiesS = objectManager.GetPool("EnemyS");
        for (int index = 0; index < enemiesL.Length; index++)
            if (enemiesL[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesL[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesM.Length; index++)
            if (enemiesM[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesM[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesS.Length; index++)
            if (enemiesS[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesS[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }

        //모든 적 총알 제거
        GameObject[] bulletsA = objectManager.GetPool("BulletEnemyA");
        GameObject[] bulletsB = objectManager.GetPool("BulletEnemyB");
        for (int index = 0; index < bulletsA.Length; index++)
            if (bulletsA[index].activeSelf)     //활성화된 총알만 제거
                bulletsA[index].SetActive(false);
        for (int index = 0; index < bulletsB.Length; index++)
            if (bulletsB[index].activeSelf)     //활성화된 총알만 제거
                bulletsB[index].SetActive(false);
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch(collision.gameObject.name) {
                case "Top":
                    isTouchTop = true;
                    break;
                case "Bottom":
                    isTouchBottom = true;
                    break;
                case "Left":
                    isTouchLeft = true;
                    break;
                case "Right":
                    isTouchRight = true;
                    break;
            }
        }
        else if (collision.gameObject.tag == "Enemy" || collision.gameObject.tag == "EnemyBullet") {
            if (isRespawnTime)  //무적 시간일때
                return;

            if (isHit)  //이미 맞으면 다시 맞지 않도록 return
                return;

            isHit = true;
            life--;
            gameManager.UpdateLifeIcon(life);
            gameManager.CallExplosion(transform.position, "P");

            if (life == 0) {
                gameManager.GameOver();
            }
            else {
                gameManager.RespawnPlayer();
            }
            gameObject.SetActive(false);
            if (collision.gameObject.tag == "Enemy") {
                Enemy enemyObject = collision.gameObject.GetComponent<Enemy>();
                if (enemyObject.enemyName == "B")
                    return;
                else
                    collision.gameObject.SetActive(false);
            }
        }
        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++;
                        AddFollower();
                    }
                    break;
                case "Boom":
                    if (boom == maxBoom)
                        score += 500;
                    else {
                        boom++;
                        gameManager.UpdateBoomIcon(boom);
                    }
                    break;
            }

            collision.gameObject.SetActive(false);
        }
    }

    void OffBoomEffect() {
        boomEffect.SetActive(false);
        isBoomTime = false;
    }

    void AddFollower() {
        if (power == 4)
            followers[0].SetActive(true);
        else if (power == 5)
            followers[1].SetActive(true);
        else if (power == 6)
            followers[2].SetActive(true);

    }

    void OnTriggerExit2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch (collision.gameObject.name) {
                case "Top":
                    isTouchTop = false;
                    break;
                case "Bottom":
                    isTouchBottom = false;
                    break;
                case "Left":
                    isTouchLeft = false;
                    break;
                case "Right":
                    isTouchRight = false;
                    break;
            }
        }
    }
}
//Enemy 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour {
    public string enemyName;
    public int enemyScore;
    public float speed;
    public int health;
    public float maxShotDelay;
    public float curShotDelay;
    public int patternIndex;
    public int curPatternCount;
    public int[] maxPatternCount;

    public GameObject bulletObjA;
    public GameObject bulletObjB;
    public GameObject itemCoin;
    public GameObject itemPower;
    public GameObject itemBoom;
    public GameObject player;
    public GameManager gameManager;
    public ObjectManager objectManager;
    public Sprite[] sprites;

    SpriteRenderer spriteRenderer;
    Animator anim;

    void Awake() {
        spriteRenderer = GetComponent<SpriteRenderer>();

        if (enemyName == "B")
            anim = GetComponent<Animator>();
    }

    void OnEnable() {
        switch(enemyName) {
            case "B":
                health = 3000;
                Invoke("Stop", 2);
                break;
            case "L":
                health = 40;
                break;
            case "M":
                health = 10;
                break;
            case "S":
                health = 3;
                break;
        }
    }

    void Stop() {
        if (!gameObject.activeSelf)
            return;

        Rigidbody2D rigid = GetComponent<Rigidbody2D>();
        rigid.velocity = Vector2.zero;

        Invoke("Think", 2);
    }

    void Think() {
        patternIndex = (patternIndex == 3) ? 0 : patternIndex + 1;
        curPatternCount = 0;

        switch (patternIndex) {
            case 0:
                FireFoward();
                break;
            case 1:
                FireShot();
                break;
            case 2:
                FireArc();
                break;
            case 3:
                FireAround();
                break;
        }
    }

    void FireFoward() {
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //총알 앞으로 4발 발사
        GameObject bulletR = objectManager.MakeObj("BulletBossA");
        bulletR.transform.position = transform.position + Vector3.right * 0.65f + Vector3.down * 1;
        GameObject bulletRR = objectManager.MakeObj("BulletBossA");
        bulletRR.transform.position = transform.position + Vector3.right * 0.8f + Vector3.down * 1;
        GameObject bulletL = objectManager.MakeObj("BulletBossA");
        bulletL.transform.position = transform.position + Vector3.left * 0.65f + Vector3.down * 1;
        GameObject bulletLL = objectManager.MakeObj("BulletBossA");
        bulletLL.transform.position = transform.position + Vector3.left * 0.8f + Vector3.down * 1;

        Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();

        rigidR.AddForce(Vector2.down * 8, ForceMode2D.Impulse);
        rigidRR.AddForce(Vector2.down * 8, ForceMode2D.Impulse);
        rigidL.AddForce(Vector2.down * 8, ForceMode2D.Impulse);
        rigidLL.AddForce(Vector2.down * 8, ForceMode2D.Impulse);

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireFoward", 2);
        else
            Invoke("Think", 3);
    }

    void FireShot() {
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //플레이어를 향해 방사형 발사
        for (int index = 0; index < 5; index++) {
            GameObject bullet = objectManager.MakeObj("BulletEnemyB");
            bullet.transform.position = transform.position;

            Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
            Vector2 dirVec = player.transform.position - transform.position;
            Vector2 ranVec = new Vector2(Random.Range(-0.5f,0.5f), Random.Range(0f, 2f));
            dirVec += ranVec;
            rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
        }

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireShot", 3.5f);
        else
            Invoke("Think", 3);
    }

    void FireArc() {   
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //부채 형태로 연속 발사
        GameObject bullet = objectManager.MakeObj("BulletEnemyA");
        bullet.transform.position = transform.position;
        bullet.transform.rotation = Quaternion.identity;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec = new Vector2(Mathf.Cos(Mathf.PI * 10 * curPatternCount/maxPatternCount[patternIndex]),-1);
        rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireArc", 0.15f);
        else
            Invoke("Think", 3);
    }

    void FireAround() {
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //원 형태로 광역 공격
        int roundNumA = 50;
        int roundNumB = 40;
        int roundNum = (curPatternCount % 2 == 0) ? roundNumA : roundNumB;

        for (int index = 0; index < roundNum; index++) {
            GameObject bullet = objectManager.MakeObj("BulletBossB");
            bullet.transform.position = transform.position;
            bullet.transform.rotation = Quaternion.identity;

            Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
            Vector2 dirVec = new Vector2(Mathf.Cos(Mathf.PI * 2 * index / roundNum), Mathf.Sin(Mathf.PI * 2 * index / roundNum));
            rigid.AddForce(dirVec.normalized * 2, ForceMode2D.Impulse);

            Vector3 rotVec = Vector3.forward * 360 * index / roundNum + Vector3.forward * 90;
            bullet.transform.Rotate(rotVec);
        }

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireAround", 0.7f);
        else
            Invoke("Think", 3);
    }

    void Update() {
        if (enemyName == "B")
            return;

        Fire();
        Reload();
    }

    void Fire() {
        if (curShotDelay < maxShotDelay)    //장전 중이라면
            return;

        if (enemyName == "S") {
            GameObject bullet = objectManager.MakeObj("BulletEnemyA");
            bullet.transform.position = transform.position;
            Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
            Vector3 dirVec = player.transform.position - transform.position;    //목표물로 방향 = 목표물 위치 - 자신의 위치
            rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
        }
        else if (enemyName == "L") {
            GameObject bulletR = objectManager.MakeObj("BulletEnemyB");
            bulletR.transform.position = transform.position + Vector3.right * 0.3f;
            GameObject bulletL = objectManager.MakeObj("BulletEnemyB");
            bulletL.transform.position = transform.position + Vector3.left * 0.3f;
            Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
            Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();

            Vector3 dirVecR = player.transform.position - (transform.position + Vector3.right * 0.3f);
            Vector3 dirVecL = player.transform.position - (transform.position + Vector3.left * 0.3f);

            rigidR.AddForce(dirVecR.normalized * 4, ForceMode2D.Impulse);
            rigidL.AddForce(dirVecL.normalized * 4, ForceMode2D.Impulse);
        }

        curShotDelay = 0;
    }

    void Reload() {
        curShotDelay += Time.deltaTime;
    }

    public void onHit(int dmg) {
        if (health <= 0)    //이미 죽은 상태라면
            return;

        health -= dmg;
        if (enemyName == "B") {
            anim.SetTrigger("OnHit");
        }
        else {
            spriteRenderer.sprite = sprites[1]; //평소 스프라이트 0, 피격시 스프라이트 1
            Invoke("ReturnSprite", 0.1f);
        }

        if (health <= 0) {
            Player playerLogic = player.GetComponent<Player>();
            playerLogic.score += enemyScore;

            //랜덤 아이템 드랍. 보스는 드랍하지 않음
            int ran = (enemyName == "B") ? 0 : Random.Range(0, 10);
            if (ran < 3) {  //드랍 없음 30%
                //아이템 없음
            }
            else if (ran < 6) { //동전 드랍 30%
                GameObject itemCoin = objectManager.MakeObj("ItemCoin");
                itemCoin.transform.position = transform.position;
            }
            else if (ran < 8) {     //파워 드랍 20%
                GameObject itemPower = objectManager.MakeObj("ItemPower");
                itemPower.transform.position = transform.position;
            }
            else if (ran < 10) {    //붐 드랍 20%
                GameObject itemBoom = objectManager.MakeObj("ItemBoom");
                itemBoom.transform.position = transform.position;
            }

            if (enemyName == "B")
                CancelInvoke();
            gameObject.SetActive(false);
            transform.rotation = Quaternion.identity;
            gameManager.CallExplosion(transform.position, enemyName);
        }
    }

    void ReturnSprite() {
        spriteRenderer.sprite = sprites[0];
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "BorderBullet" && enemyName != "B") {   //보스가 아닌 적이 맵 경계로 가면
            gameObject.SetActive(false);
            transform.rotation = Quaternion.identity;
        }
        else if (collision.gameObject.tag == "PlayerBullet")
        {    //플레이어 총알에 닿으면
            Bullet bullet = collision.gameObject.GetComponent<Bullet>();
            onHit(bullet.dmg);
            collision.gameObject.SetActive(false);  //플레이어 총알 삭제
        }
    }
}

Object Manager에 프리펩 추가
폭발 이펙트 발동하는 모습
크기도 잘 적용되었다

  • Explosion이 프리펩이 되었으니 ObjectManager을 통해 오브젝트 풀로 등록해준다.
  • 그리고 GameManager에 폭발 이펙트를 불러오는 함수를 만들고, 이를 Player와 Enemy가 파괴될 때 사용한다. Enemy는 프리펩이므로 이 함수를 사용할 수 있도록 GameManager에서 자기 자신(this)을 넘겨준다.

Explosion Animator

  • 참고로 폭발 애니메이션이 중간에 잘려서 위 캡처처럼 Animator의 Exit Time을 1로 해서 애니메이션이 완전하게 출력되도록 임의로 변경해주었다.

3. 모바일 컨트롤 UI

작업을 위해서 Camera로 변경

  • Render Mode에서 Overlay는 기존 방식처럼 UI가 따로 보이는 것이고, Camera는 Game 화면처럼 같이 보이는 것이다. Camera로 바뀌면 Render Camera에 MainCamera를 끌어넣어줘야 한다.

Canvas 설정과 Render Mode가 변경된 모습
움직일때 사용할 UI Image인 Joy Pad 추가 및 설정
Joy Pad 앵커를 좌측하단으로 하고, 알파값을 120정도로 하여 반투명하게 만든다
Joy Pad 안에 Button 생성
Button의 Text를 제거하고, 이미지가 보이지않도록 투명한 Button을 만든다
투명한 버튼을 총 9개 만들어서 3x3 형태로 배치해준다. 앵커도 각 위치에 맞게 설정한다.

  • 버튼을 이렇게 만든 이유는 플레이어가 패드를 누를 때 대각선 이동도 가능하게 하기 위함이다.

각 버튼에 Event Trigger 컴포넌트 추가

//Player 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {
    public GameManager gameManager;
    public ObjectManager objectManager;
    public GameObject bulletObjA;
    public GameObject bulletObjB;
    public GameObject boomEffect;
    public GameObject[] followers;
    Animator anim;
    SpriteRenderer spriteRenderer;

    public int life;
    public int score;
    public float speed;
    public int maxPower;
    public int power;
    public int maxBoom;
    public int boom;
    public float maxShotDelay;
    public float curShotDelay;

    public bool isTouchTop;
    public bool isTouchBottom;
    public bool isTouchLeft;
    public bool isTouchRight;
    public bool isHit;
    public bool isBoomTime;

    public bool isRespawnTime;

    public bool[] joyControl;
    public bool isControl;

    void Awake() {
        anim = GetComponent<Animator>();
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    void OnEnable() {
        Unbeatable();
        Invoke("Unbeatable", 3);
    }

    void Unbeatable() {
        isRespawnTime = !isRespawnTime; //값을 반대로

        if (isRespawnTime) {    //무적 시간 (반투명화)
            isRespawnTime = true;
            spriteRenderer.color = new Color(1, 1, 1, 0.5f);    //플레이어 스프라이트

            for (int index = 0; index < followers.Length; index++) {    //팔로워 스프라이트
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0.5f);
            }
        }
        else {  //무적 시간 종료 (원래대로)
            isRespawnTime = false;
            spriteRenderer.color = new Color(1, 1, 1, 1);

            for (int index = 0; index < followers.Length; index++) {
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 1);
            }
        }
    }

    void Update() {
        Move();
        Fire();
        Boom();
        Reload();
    }

    public void JoyPad(int type) {
        for (int index = 0; index < 9; index++) {
            joyControl[index] = (index == type);
        }
    }

    public void JoyDown() {
        isControl = true;
    }

    public void JoyUp() {
        isControl = false;
    }

    void Move() {   //플레이어 이동 함수
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");

        if (joyControl[0]) { h = -1; v = 1; }
        if (joyControl[1]) { h = 0; v = 1; }
        if (joyControl[2]) { h = 1; v = 1; }
        if (joyControl[3]) { h = -1; v = 0; }
        if (joyControl[4]) { h = 0; v = 0; }
        if (joyControl[5]) { h = 1; v = 0; }
        if (joyControl[6]) { h = -1; v = -1; }
        if (joyControl[7]) { h = 0; v = -1; }
        if (joyControl[8]) { h = 1; v = -1; }

        if ((isTouchRight && h == 1) || (isTouchLeft && h == -1) || !isControl)
            h = 0;
        if ((isTouchTop && v == 1) || (isTouchBottom && v == -1) || !isControl)
            v = 0;

        Vector3 curPos = transform.position;
        Vector3 nextPos = new Vector3(h, v, 0) * speed * Time.deltaTime;

        transform.position = curPos + nextPos;

        if (Input.GetButtonDown("Horizontal") || Input.GetButtonUp("Horizontal"))
            anim.SetInteger("Input", (int)h);
    }

    void Fire() {   //플레이어 총알 발사 함수
        if (!Input.GetButton("Fire1"))  //Ctrl 키, 마우스 좌클릭
            return;

        if (curShotDelay < maxShotDelay)    //장전 중이라면
            return;

        switch(power) {
            case 1:
                GameObject bullet = objectManager.MakeObj("BulletPlayerA");
                bullet.transform.position = transform.position;
                Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
                rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 2:
                GameObject bulletR = objectManager.MakeObj("BulletPlayerA");
                bulletR.transform.position = transform.position + Vector3.right * 0.1f;
                GameObject bulletL = objectManager.MakeObj("BulletPlayerA");
                bulletL.transform.position = transform.position + Vector3.left * 0.1f;
                Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
                rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            default:
                GameObject bulletRR = objectManager.MakeObj("BulletPlayerA");
                bulletRR.transform.position = transform.position + Vector3.right * 0.35f;
                GameObject bulletCC = objectManager.MakeObj("BulletPlayerB");
                bulletCC.transform.position = transform.position;
                GameObject bulletLL = objectManager.MakeObj("BulletPlayerA");
                bulletLL.transform.position = transform.position + Vector3.left * 0.35f;
                Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidCC = bulletCC.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();
                rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
        }
        
        curShotDelay = 0;
    }

    void Reload() {
        curShotDelay += Time.deltaTime;
    }

    void Boom() {
        if (!Input.GetButton("Fire2"))  //Alt 키, 마우스 우클릭
            return;

        if (isBoomTime) //Boom 재사용 대기시간 중이라면
            return;

        if (boom == 0)  //필살기가 없다면
            return;

        boom--;
        isBoomTime = true;
        gameManager.UpdateBoomIcon(boom);

        //Boom 활성화
        boomEffect.SetActive(true);
        Invoke("OffBoomEffect", 4f);

        //모든 적 처치
        GameObject[] enemiesL = objectManager.GetPool("EnemyL");
        GameObject[] enemiesM = objectManager.GetPool("EnemyM");
        GameObject[] enemiesS = objectManager.GetPool("EnemyS");
        for (int index = 0; index < enemiesL.Length; index++)
            if (enemiesL[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesL[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesM.Length; index++)
            if (enemiesM[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesM[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesS.Length; index++)
            if (enemiesS[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesS[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }

        //모든 적 총알 제거
        GameObject[] bulletsA = objectManager.GetPool("BulletEnemyA");
        GameObject[] bulletsB = objectManager.GetPool("BulletEnemyB");
        for (int index = 0; index < bulletsA.Length; index++)
            if (bulletsA[index].activeSelf)     //활성화된 총알만 제거
                bulletsA[index].SetActive(false);
        for (int index = 0; index < bulletsB.Length; index++)
            if (bulletsB[index].activeSelf)     //활성화된 총알만 제거
                bulletsB[index].SetActive(false);
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch(collision.gameObject.name) {
                case "Top":
                    isTouchTop = true;
                    break;
                case "Bottom":
                    isTouchBottom = true;
                    break;
                case "Left":
                    isTouchLeft = true;
                    break;
                case "Right":
                    isTouchRight = true;
                    break;
            }
        }
        else if (collision.gameObject.tag == "Enemy" || collision.gameObject.tag == "EnemyBullet") {
            if (isRespawnTime)  //무적 시간일때
                return;

            if (isHit)  //이미 맞으면 다시 맞지 않도록 return
                return;

            isHit = true;
            life--;
            gameManager.UpdateLifeIcon(life);
            gameManager.CallExplosion(transform.position, "P");

            if (life == 0) {
                gameManager.GameOver();
            }
            else {
                gameManager.RespawnPlayer();
            }
            gameObject.SetActive(false);
            if (collision.gameObject.tag == "Enemy") {
                Enemy enemyObject = collision.gameObject.GetComponent<Enemy>();
                if (enemyObject.enemyName == "B")
                    return;
                else
                    collision.gameObject.SetActive(false);
            }
        }
        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++;
                        AddFollower();
                    }
                    break;
                case "Boom":
                    if (boom == maxBoom)
                        score += 500;
                    else {
                        boom++;
                        gameManager.UpdateBoomIcon(boom);
                    }
                    break;
            }

            collision.gameObject.SetActive(false);
        }
    }

    void OffBoomEffect() {
        boomEffect.SetActive(false);
        isBoomTime = false;
    }

    void AddFollower() {
        if (power == 4)
            followers[0].SetActive(true);
        else if (power == 5)
            followers[1].SetActive(true);
        else if (power == 6)
            followers[2].SetActive(true);

    }

    void OnTriggerExit2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch (collision.gameObject.name) {
                case "Top":
                    isTouchTop = false;
                    break;
                case "Bottom":
                    isTouchBottom = false;
                    break;
                case "Left":
                    isTouchLeft = false;
                    break;
                case "Right":
                    isTouchRight = false;
                    break;
            }
        }
    }
}

Button LT 설정
조이패드를 클릭하니 이동이 된다

  • Button LT 외 다른 버튼 8개도 컴포넌트 값(value) 복사를 통해서 Pointer Enter의 변수 값(1~8)만 다르게 설정한다. 

총알 발사 버튼 A 추가 및 설정
필살기 버튼 B 추가 및 설정

//Player 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {
    public GameManager gameManager;
    public ObjectManager objectManager;
    public GameObject bulletObjA;
    public GameObject bulletObjB;
    public GameObject boomEffect;
    public GameObject[] followers;
    Animator anim;
    SpriteRenderer spriteRenderer;

    public int life;
    public int score;
    public float speed;
    public int maxPower;
    public int power;
    public int maxBoom;
    public int boom;
    public float maxShotDelay;
    public float curShotDelay;

    public bool isTouchTop;
    public bool isTouchBottom;
    public bool isTouchLeft;
    public bool isTouchRight;
    public bool isHit;
    public bool isBoomTime;

    public bool isRespawnTime;

    public bool[] joyControl;
    public bool isControl;
    public bool isButtonA;
    public bool isButtonB;

    void Awake() {
        anim = GetComponent<Animator>();
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    void OnEnable() {
        Unbeatable();
        Invoke("Unbeatable", 3);
    }

    void Unbeatable() {
        isRespawnTime = !isRespawnTime; //값을 반대로

        if (isRespawnTime) {    //무적 시간 (반투명화)
            isRespawnTime = true;
            spriteRenderer.color = new Color(1, 1, 1, 0.5f);    //플레이어 스프라이트

            for (int index = 0; index < followers.Length; index++) {    //팔로워 스프라이트
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0.5f);
            }
        }
        else {  //무적 시간 종료 (원래대로)
            isRespawnTime = false;
            spriteRenderer.color = new Color(1, 1, 1, 1);

            for (int index = 0; index < followers.Length; index++) {
                followers[index].GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 1);
            }
        }
    }

    void Update() {
        Move();
        Fire();
        Boom();
        Reload();
    }

    public void JoyPad(int type) {
        for (int index = 0; index < 9; index++) {
            joyControl[index] = (index == type);
        }
    }

    public void JoyDown() {
        isControl = true;
    }

    public void JoyUp() {
        isControl = false;
    }

    void Move() {   //플레이어 이동 함수
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");

        if (joyControl[0]) { h = -1; v = 1; }
        if (joyControl[1]) { h = 0; v = 1; }
        if (joyControl[2]) { h = 1; v = 1; }
        if (joyControl[3]) { h = -1; v = 0; }
        if (joyControl[4]) { h = 0; v = 0; }
        if (joyControl[5]) { h = 1; v = 0; }
        if (joyControl[6]) { h = -1; v = -1; }
        if (joyControl[7]) { h = 0; v = -1; }
        if (joyControl[8]) { h = 1; v = -1; }

        if ((isTouchRight && h == 1) || (isTouchLeft && h == -1) || !isControl)
            h = 0;
        if ((isTouchTop && v == 1) || (isTouchBottom && v == -1) || !isControl)
            v = 0;

        Vector3 curPos = transform.position;
        Vector3 nextPos = new Vector3(h, v, 0) * speed * Time.deltaTime;

        transform.position = curPos + nextPos;

        if (Input.GetButtonDown("Horizontal") || Input.GetButtonUp("Horizontal"))
            anim.SetInteger("Input", (int)h);
    }

    public void ButtonADown() {
        isButtonA = true;
    }

    public void ButtonAUp() {
        isButtonA = false;
    }

    public void ButtonBDown() {
        isButtonB = true;
    }

    void Fire() {   //플레이어 총알 발사 함수
        //if (!Input.GetButton("Fire1"))  //Ctrl 키, 마우스 좌클릭
        //    return;

        if (!isButtonA) //발사 버튼을 누르지 않았다면
            return;

        if (curShotDelay < maxShotDelay)    //장전 중이라면
            return;

        switch(power) {
            case 1:
                GameObject bullet = objectManager.MakeObj("BulletPlayerA");
                bullet.transform.position = transform.position;
                Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
                rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 2:
                GameObject bulletR = objectManager.MakeObj("BulletPlayerA");
                bulletR.transform.position = transform.position + Vector3.right * 0.1f;
                GameObject bulletL = objectManager.MakeObj("BulletPlayerA");
                bulletL.transform.position = transform.position + Vector3.left * 0.1f;
                Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
                rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            default:
                GameObject bulletRR = objectManager.MakeObj("BulletPlayerA");
                bulletRR.transform.position = transform.position + Vector3.right * 0.35f;
                GameObject bulletCC = objectManager.MakeObj("BulletPlayerB");
                bulletCC.transform.position = transform.position;
                GameObject bulletLL = objectManager.MakeObj("BulletPlayerA");
                bulletLL.transform.position = transform.position + Vector3.left * 0.35f;
                Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidCC = bulletCC.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();
                rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
        }
        
        curShotDelay = 0;
    }

    void Reload() {
        curShotDelay += Time.deltaTime;
    }

    void Boom() {
        //if (!Input.GetButton("Fire2"))  //Alt 키, 마우스 우클릭
        //    return;

        if (!isButtonB) //필살기 버튼을 누르지 않았다면
            return;

        if (isBoomTime) //Boom 재사용 대기시간 중이라면
            return;

        if (boom == 0)  //필살기가 없다면
            return;

        boom--;
        isBoomTime = true;
        isButtonB = false;
        gameManager.UpdateBoomIcon(boom);
        
        //Boom 활성화
        boomEffect.SetActive(true);
        Invoke("OffBoomEffect", 4f);

        //모든 적 처치
        GameObject[] enemiesL = objectManager.GetPool("EnemyL");
        GameObject[] enemiesM = objectManager.GetPool("EnemyM");
        GameObject[] enemiesS = objectManager.GetPool("EnemyS");
        for (int index = 0; index < enemiesL.Length; index++)
            if (enemiesL[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesL[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesM.Length; index++)
            if (enemiesM[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesM[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }
        for (int index = 0; index < enemiesS.Length; index++)
            if (enemiesS[index].activeSelf) {   //활성화된 적만 처치
                Enemy enemyLogic = enemiesS[index].GetComponent<Enemy>();
                enemyLogic.onHit(1000);
            }

        //모든 적 총알 제거
        GameObject[] bulletsA = objectManager.GetPool("BulletEnemyA");
        GameObject[] bulletsB = objectManager.GetPool("BulletEnemyB");
        for (int index = 0; index < bulletsA.Length; index++)
            if (bulletsA[index].activeSelf)     //활성화된 총알만 제거
                bulletsA[index].SetActive(false);
        for (int index = 0; index < bulletsB.Length; index++)
            if (bulletsB[index].activeSelf)     //활성화된 총알만 제거
                bulletsB[index].SetActive(false);
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch(collision.gameObject.name) {
                case "Top":
                    isTouchTop = true;
                    break;
                case "Bottom":
                    isTouchBottom = true;
                    break;
                case "Left":
                    isTouchLeft = true;
                    break;
                case "Right":
                    isTouchRight = true;
                    break;
            }
        }
        else if (collision.gameObject.tag == "Enemy" || collision.gameObject.tag == "EnemyBullet") {
            if (isRespawnTime)  //무적 시간일때
                return;

            if (isHit)  //이미 맞으면 다시 맞지 않도록 return
                return;

            isHit = true;
            life--;
            gameManager.UpdateLifeIcon(life);
            gameManager.CallExplosion(transform.position, "P");

            if (life == 0) {
                gameManager.GameOver();
            }
            else {
                gameManager.RespawnPlayer();
            }
            gameObject.SetActive(false);
            if (collision.gameObject.tag == "Enemy") {
                Enemy enemyObject = collision.gameObject.GetComponent<Enemy>();
                if (enemyObject.enemyName == "B")
                    return;
                else
                    collision.gameObject.SetActive(false);
            }
        }
        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++;
                        AddFollower();
                    }
                    break;
                case "Boom":
                    if (boom == maxBoom)
                        score += 500;
                    else {
                        boom++;
                        gameManager.UpdateBoomIcon(boom);
                    }
                    break;
            }

            collision.gameObject.SetActive(false);
        }
    }

    void OffBoomEffect() {
        boomEffect.SetActive(false);
        isBoomTime = false;
    }

    void AddFollower() {
        if (power == 4)
            followers[0].SetActive(true);
        else if (power == 5)
            followers[1].SetActive(true);
        else if (power == 6)
            followers[2].SetActive(true);

    }

    void OnTriggerExit2D(Collider2D collision) {
        if (collision.gameObject.tag == "Border") {
            switch (collision.gameObject.name) {
                case "Top":
                    isTouchTop = false;
                    break;
                case "Bottom":
                    isTouchBottom = false;
                    break;
                case "Left":
                    isTouchLeft = false;
                    break;
                case "Right":
                    isTouchRight = false;
                    break;
            }
        }
    }
}

Button A 설정
Button B 설정
A 버튼을 누르니 총알 발사가 잘 된다
B버튼을 누르니 필살기도 잘 사용된다

  • 앞의 패드와 비슷하게 bool형 변수들을 사용해서 클릭/클릭하지 않음으로 구분하여 총알과 필살기를 사용하도록 만들었다.

+) 추가 수정

//Follower 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Follower : MonoBehaviour {
    public float maxShotDelay;
    public float curShotDelay;
    public ObjectManager objectManager;
    public Player player;

    public Vector3 followPos;
    public int followDelay;
    public Transform parent;
    public Queue<Vector3> parentPos;

    void Awake() {
        parentPos = new Queue<Vector3>();
    }

    void Update() {
        Watch();
        Follow();
        Fire();
        Reload();
    }

    void Watch() {
        //위치 입력
        if (!parentPos.Contains(parent.position))   //같은 위치값이면 큐에 저장하지 않음
            parentPos.Enqueue(parent.position);

        //위치 출력
        if (parentPos.Count > followDelay)
            followPos = parentPos.Dequeue();
        else if (parentPos.Count < followDelay)
            followPos = parent.position;
    }

    void Follow() {
        transform.position = followPos;
    }

    void Fire() {
        //if (!Input.GetButton("Fire1"))
        //    return;

        if (!player.isButtonA)  //플레이어가 버튼 A를 누르지 않았다면
            return;

        if (curShotDelay < maxShotDelay)
            return;

        GameObject bullet = objectManager.MakeObj("BulletFollower");
        bullet.transform.position = transform.position;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);

        curShotDelay = 0;
    }

    void Reload() {
        curShotDelay += Time.deltaTime;
    }
}

Follower들 설정
자세히 보면 플레이어와 비슷한 시점에 발사한다

  • follower가 버튼 A가 아닌 마우스 클릭을 인식하고 발사되어서 Player를 불러와 isButtonA에 따라서 발사하도록 수정했다.

4. 스테이지 관리

Resources의 Stage 0를 복사
Stage 0는 삭제해서 1번부터 넘버링을 시작하자
스테이지 시작을 알릴 UI Text 생성 및 설정
스테이지가 끝났음을 알릴 UI Text 추가 생성 및 설정.
위 Text에 사용할 애니메이터와 애니메이션 생성
앞서 만든 Text UI들에 애니메이터를 끌어다 넣어서 컴포넌트를 추가해준다
Text Animator 설정

  • On은 트리거형 매개변수다. 이전 애니메이션들과 마찬가지로 Idle은 Empty이고, Text -> Idle Transition은 Transition Duration만 0으로 설정해둔다.

Text Animation 설정

  • Text Animation은 텍스트의 Scale, 즉 크기만을 이용하여 구현할 것이다.
  • 각 키 프레임이 0초에는 크기가 0/0/0으로 시작해 0.3초에 1.2/1.2/1.2, 이후 0.4초에 다시 1/1/1, 이후 5초까지 유지하다가 5.1초에 1.2/1.2/1.2, 마지막 5.3초에 0/0/0이 되어 사라진다. 이렇게 구현해서 실행해보면 텍스트가 약간 통통 튀듯이 나타났다가 사라지게 된다.

UI Text 설정. 비활성화가 아닌 Scale을 0으로 만들어준다.

  • 앞의 애니메이션이 Scale을 다루는 것이므로, 자연스럽게 Text도 활성화/비활성화 하는 것이 아닌 Scale을 0/0/0으로 만들어준다.

애니메이션이 길어짐에따라 Transition이 겹치는 부분이 없도록 조정한다
Transition 조정

  • 앞에서 폭발 이펙트에서 다뤘듯이 Transition이 겹치면 애니메이션이 정상적으로 출력되지 않고 일부 잘리는 일이 발생하므로 Exit Time을 1로 만들어준다. AnyState -> Text의 경우 Exit Time이 비활성화되어있으므로 아래 프레임을 이용하거나 잠시 Has Exit Time을 체크해서 바꾸는 걸로 수정할 수 있다.

스테이지 전환(페이드 인, 페이드 아웃)에 사용할 스프라이트 추가. 화면을 덮을정도로 크게 만들고 Order in Layer를 설정한다.
Fade 애니메이터와 In, Out 애니메이션 2개 생성
Fade Animator Transition 설정
Fad Out Transition 설정
Sprite Renderer의 Color을 사용한다.
Fade In은 간단하게 1초에 걸쳐서 알파값만 0이 되도록 만들어준다
Fade Out은 반대로 알파값이 0에서 1로 투명도를 낮아지게 만들어준다

  • 어차피 Fade In을 통해서 검은 화면이 사라지고, Fade Out을 통해서 검은 화면이 생길테니 따로 Idle에 Transition을 연결할 필요가 없다.

Player Position 생성 및 설정

  • 각 스테이지가 끝나고 다시 시작될 때, 플레이어 위치를 초기화 해주어야하므로 이를 설정할 Empty를 하나 만든다.
  • 이제 모든 UI 요소들이 완성되었으니 스크립트 작업을 하면 된다.
//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.IO;    //파일 읽기용 라이브러리. C#에서 지원

public class GameManager : MonoBehaviour {
    public int stage;
    public Animator stageAnim;
    public Animator clearAnim;
    public Animator fadeAnim;
    public Transform playerPos;

    string[] enemyObjs;
    public Transform[] spawnPoints;
    public GameObject player;
    public Text scoreText;
    public Image[] lifeImage;
    public Image[] boomImage;
    public GameObject gameOverSet;
    public ObjectManager objectManager;

    public float nextSpawnDelay;
    public float curSpawnDelay;

    public List<Spawn> spawnList;
    public int spawnIndex;
    public bool spawnEnd;

    void Awake() {
        enemyObjs = new string[] { "EnemyS", "EnemyM", "EnemyL", "EnemyB" };
        spawnList = new List<Spawn>();
        StageStart();
    }

    public void StageStart() {
        //스테이지 UI 불러오기
        stageAnim.SetTrigger("On");
        stageAnim.GetComponent<Text>().text = "Stage " + stage + "\nStart";
        clearAnim.GetComponent<Text>().text = "Stage " + stage + "\nClear!!!";

        //적 기체 소환 파일 읽기
        ReadSpawnFile();

        //페이드 인
        fadeAnim.SetTrigger("In");
    }

    public void StageEnd() {
        //클리어 UI 불러오기
        clearAnim.SetTrigger("On");

        //페이드 아웃
        fadeAnim.SetTrigger("Out");

        //플레이어 위치 잡아주기
        player.transform.position = playerPos.position;

        //다음 스테이지로
        stage++;
        if (stage > 2)
            Invoke("GameOver", 6);
        else
            Invoke("StageStart", 5);
    }

    void ReadSpawnFile() {
        //변수 초기화
        spawnList.Clear();
        spawnIndex = 0;
        spawnEnd = false;

        //텍스트 파일 읽기
        TextAsset textFile = Resources.Load("Stage " + stage) as TextAsset;    //텍스트 파일 Stage N 불러오기
        StringReader stringReader = new StringReader(textFile.text);

        while (stringReader != null) {
            string line = stringReader.ReadLine();

            if (line == null)
                break;

            //스폰 데이터 생성
            Spawn spawnData = new Spawn();
            spawnData.delay = float.Parse(line.Split(',')[0]);
            spawnData.type = line.Split(',')[1];
            spawnData.point = int.Parse(line.Split(',')[2]);
            spawnList.Add(spawnData);
        }

        //텍스트 파일 닫기
        stringReader.Close();

        //첫번째 스폰 딜레이 적용
        nextSpawnDelay = spawnList[0].delay;
    }

    void Update() {
        curSpawnDelay += Time.deltaTime;

        if (curSpawnDelay > nextSpawnDelay && !spawnEnd) {
            SpawnEnemy();
            curSpawnDelay = 0;
        }

        //UI 점수 업데이트
        Player playerLogic = player.GetComponent<Player>();
        scoreText.text = string.Format("{0:n0}", playerLogic.score);
    }

    void SpawnEnemy() {
        int enemyIndex = 0;
        switch(spawnList[spawnIndex].type) {
            case "S":
                enemyIndex = 0;
                break;
            case "M":
                enemyIndex = 1;
                break;
            case "L":
                enemyIndex = 2;
                break;
            case "B":
                enemyIndex = 3;
                break;
        }
        int enemyPoint = spawnList[spawnIndex].point;
        GameObject enemy = objectManager.MakeObj(enemyObjs[enemyIndex]);
        enemy.transform.position = spawnPoints[enemyPoint].position;
        Rigidbody2D rigid = enemy.GetComponent<Rigidbody2D>();
        Enemy enemyLogic = enemy.GetComponent<Enemy>();
        enemyLogic.player = player; //플레이어 클래스 넘겨주기
        enemyLogic.gameManager = this;  //클래스 자신을 넘겨주기
        enemyLogic.objectManager = objectManager;   //오브젝트 매니저 클래스 넘겨주기

        if (enemyPoint == 5 || enemyPoint == 6) {   //오른쪽 포인트
            enemy.transform.Rotate(Vector3.back * 90);  //적 기체 스프라이트 돌리기
            rigid.velocity = new Vector2(enemyLogic.speed * (-1), -1);
        }
        else if (enemyPoint == 7 || enemyPoint == 8) {  //왼쪽 포인트
            enemy.transform.Rotate(Vector3.back * (-90));  //적 기체 스프라이트 돌리기
            rigid.velocity = new Vector2(enemyLogic.speed, -1);
        }
        else {  //중앙 앞쪽 포인트
            rigid.velocity = new Vector2(0, enemyLogic.speed * (-1));
        }

        //스폰 인덱스 증가
        spawnIndex++;
        if (spawnIndex == spawnList.Count) {
            spawnEnd = true;
            return;
        }

        //다음 리스폰 딜레이 갱신
        nextSpawnDelay = spawnList[spawnIndex].delay;
    }

    public void UpdateLifeIcon(int life) {
        //UI 라이프 모두 투명화
        for (int index = 0; index < 3; index++) {
            lifeImage[index].color = new Color(1, 1, 1, 0);
        }

        //UI 남은 라이프 수만큼 활성화
        for (int index = 0; index < life; index++) {
            lifeImage[index].color = new Color(1, 1, 1, 1);
        }
    }

    public void UpdateBoomIcon(int boom) {
        //UI 붐 모두 투명화
        for (int index = 0; index < 3; index++) {
            boomImage[index].color = new Color(1, 1, 1, 0);
        }

        //UI 남은 붐 수만큼 활성화
        for (int index = 0; index < boom; index++) {
            boomImage[index].color = new Color(1, 1, 1, 1);
        }
    }

    public void RespawnPlayer() {
        Invoke("RespawnPlayerExe", 2f);
    }

    void RespawnPlayerExe() {
        player.transform.position = Vector3.down * 3.5f;    //플레이어 위치 초기화
        player.SetActive(true);

        Player playerLogic = player.GetComponent<Player>();
        playerLogic.isHit = false;
    }

    public void CallExplosion(Vector3 pos, string type) {
        GameObject explosion = objectManager.MakeObj("Explosion");
        Explosion explosionLogic = explosion.GetComponent<Explosion>();

        explosion.transform.position = pos; //위치 설정
        explosionLogic.startExplosion(type);    //크기 설정
    }

    public void GameOver() {
        gameOverSet.SetActive(true);
    }

    public void GameRetry() {
        SceneManager.LoadScene(0);
    }
}
//Enemy 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour {
    public string enemyName;
    public int enemyScore;
    public float speed;
    public int health;
    public float maxShotDelay;
    public float curShotDelay;
    public int patternIndex;
    public int curPatternCount;
    public int[] maxPatternCount;

    public GameObject bulletObjA;
    public GameObject bulletObjB;
    public GameObject itemCoin;
    public GameObject itemPower;
    public GameObject itemBoom;
    public GameObject player;
    public GameManager gameManager;
    public ObjectManager objectManager;
    public Sprite[] sprites;

    SpriteRenderer spriteRenderer;
    Animator anim;

    void Awake() {
        spriteRenderer = GetComponent<SpriteRenderer>();

        if (enemyName == "B")
            anim = GetComponent<Animator>();
    }

    void OnEnable() {
        switch(enemyName) {
            case "B":
                health = 3000;
                Invoke("Stop", 2);
                break;
            case "L":
                health = 40;
                break;
            case "M":
                health = 10;
                break;
            case "S":
                health = 3;
                break;
        }
    }

    void Stop() {
        if (!gameObject.activeSelf)
            return;

        Rigidbody2D rigid = GetComponent<Rigidbody2D>();
        rigid.velocity = Vector2.zero;

        Invoke("Think", 2);
    }

    void Think() {
        patternIndex = (patternIndex == 3) ? 0 : patternIndex + 1;
        curPatternCount = 0;

        switch (patternIndex) {
            case 0:
                FireFoward();
                break;
            case 1:
                FireShot();
                break;
            case 2:
                FireArc();
                break;
            case 3:
                FireAround();
                break;
        }
    }

    void FireFoward() {
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //총알 앞으로 4발 발사
        GameObject bulletR = objectManager.MakeObj("BulletBossA");
        bulletR.transform.position = transform.position + Vector3.right * 0.65f + Vector3.down * 1;
        GameObject bulletRR = objectManager.MakeObj("BulletBossA");
        bulletRR.transform.position = transform.position + Vector3.right * 0.8f + Vector3.down * 1;
        GameObject bulletL = objectManager.MakeObj("BulletBossA");
        bulletL.transform.position = transform.position + Vector3.left * 0.65f + Vector3.down * 1;
        GameObject bulletLL = objectManager.MakeObj("BulletBossA");
        bulletLL.transform.position = transform.position + Vector3.left * 0.8f + Vector3.down * 1;

        Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();

        rigidR.AddForce(Vector2.down * 8, ForceMode2D.Impulse);
        rigidRR.AddForce(Vector2.down * 8, ForceMode2D.Impulse);
        rigidL.AddForce(Vector2.down * 8, ForceMode2D.Impulse);
        rigidLL.AddForce(Vector2.down * 8, ForceMode2D.Impulse);

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireFoward", 2);
        else
            Invoke("Think", 3);
    }

    void FireShot() {
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //플레이어를 향해 방사형 발사
        for (int index = 0; index < 5; index++) {
            GameObject bullet = objectManager.MakeObj("BulletEnemyB");
            bullet.transform.position = transform.position;

            Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
            Vector2 dirVec = player.transform.position - transform.position;
            Vector2 ranVec = new Vector2(Random.Range(-0.5f,0.5f), Random.Range(0f, 2f));
            dirVec += ranVec;
            rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
        }

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireShot", 3.5f);
        else
            Invoke("Think", 3);
    }

    void FireArc() {   
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //부채 형태로 연속 발사
        GameObject bullet = objectManager.MakeObj("BulletEnemyA");
        bullet.transform.position = transform.position;
        bullet.transform.rotation = Quaternion.identity;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec = new Vector2(Mathf.Cos(Mathf.PI * 10 * curPatternCount/maxPatternCount[patternIndex]),-1);
        rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireArc", 0.15f);
        else
            Invoke("Think", 3);
    }

    void FireAround() {
        if (health <= 0)    //이미 죽은 상태라면 멈추기
            return;

        //원 형태로 광역 공격
        int roundNumA = 50;
        int roundNumB = 40;
        int roundNum = (curPatternCount % 2 == 0) ? roundNumA : roundNumB;

        for (int index = 0; index < roundNum; index++) {
            GameObject bullet = objectManager.MakeObj("BulletBossB");
            bullet.transform.position = transform.position;
            bullet.transform.rotation = Quaternion.identity;

            Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
            Vector2 dirVec = new Vector2(Mathf.Cos(Mathf.PI * 2 * index / roundNum), Mathf.Sin(Mathf.PI * 2 * index / roundNum));
            rigid.AddForce(dirVec.normalized * 2, ForceMode2D.Impulse);

            Vector3 rotVec = Vector3.forward * 360 * index / roundNum + Vector3.forward * 90;
            bullet.transform.Rotate(rotVec);
        }

        //패턴 횟수 갱신
        curPatternCount++;

        if (curPatternCount < maxPatternCount[patternIndex])
            Invoke("FireAround", 0.7f);
        else
            Invoke("Think", 3);
    }

    void Update() {
        if (enemyName == "B")
            return;

        Fire();
        Reload();
    }

    void Fire() {
        if (curShotDelay < maxShotDelay)    //장전 중이라면
            return;

        if (enemyName == "S") {
            GameObject bullet = objectManager.MakeObj("BulletEnemyA");
            bullet.transform.position = transform.position;
            Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
            Vector3 dirVec = player.transform.position - transform.position;    //목표물로 방향 = 목표물 위치 - 자신의 위치
            rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
        }
        else if (enemyName == "L") {
            GameObject bulletR = objectManager.MakeObj("BulletEnemyB");
            bulletR.transform.position = transform.position + Vector3.right * 0.3f;
            GameObject bulletL = objectManager.MakeObj("BulletEnemyB");
            bulletL.transform.position = transform.position + Vector3.left * 0.3f;
            Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
            Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();

            Vector3 dirVecR = player.transform.position - (transform.position + Vector3.right * 0.3f);
            Vector3 dirVecL = player.transform.position - (transform.position + Vector3.left * 0.3f);

            rigidR.AddForce(dirVecR.normalized * 4, ForceMode2D.Impulse);
            rigidL.AddForce(dirVecL.normalized * 4, ForceMode2D.Impulse);
        }

        curShotDelay = 0;
    }

    void Reload() {
        curShotDelay += Time.deltaTime;
    }

    public void onHit(int dmg) {
        if (health <= 0)    //이미 죽은 상태라면
            return;

        health -= dmg;
        if (enemyName == "B") {
            anim.SetTrigger("OnHit");
        }
        else {
            spriteRenderer.sprite = sprites[1]; //평소 스프라이트 0, 피격시 스프라이트 1
            Invoke("ReturnSprite", 0.1f);
        }

        if (health <= 0) {
            Player playerLogic = player.GetComponent<Player>();
            playerLogic.score += enemyScore;

            //랜덤 아이템 드랍. 보스는 드랍하지 않음
            int ran = (enemyName == "B") ? 0 : Random.Range(0, 10);
            if (ran < 3) {  //드랍 없음 30%
                //아이템 없음
            }
            else if (ran < 6) { //동전 드랍 30%
                GameObject itemCoin = objectManager.MakeObj("ItemCoin");
                itemCoin.transform.position = transform.position;
            }
            else if (ran < 8) {     //파워 드랍 20%
                GameObject itemPower = objectManager.MakeObj("ItemPower");
                itemPower.transform.position = transform.position;
            }
            else if (ran < 10) {    //붐 드랍 20%
                GameObject itemBoom = objectManager.MakeObj("ItemBoom");
                itemBoom.transform.position = transform.position;
            }

            if (enemyName == "B") {
                CancelInvoke();
                gameManager.StageEnd();
            }

            gameObject.SetActive(false);
            transform.rotation = Quaternion.identity;
            gameManager.CallExplosion(transform.position, enemyName);
        }
    }

    void ReturnSprite() {
        spriteRenderer.sprite = sprites[0];
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "BorderBullet" && enemyName != "B") {   //보스가 아닌 적이 맵 경계로 가면
            gameObject.SetActive(false);
            transform.rotation = Quaternion.identity;
        }
        else if (collision.gameObject.tag == "PlayerBullet")
        {    //플레이어 총알에 닿으면
            Bullet bullet = collision.gameObject.GetComponent<Bullet>();
            onHit(bullet.dmg);
            collision.gameObject.SetActive(false);  //플레이어 총알 삭제
        }
    }
}

GameManager 설정
실행하니 어두웠던 화면이
밝아지면서 Text UI가 떴다
보스를 처치하니 클리어 Text가 떴다
Stage 2 텍스트가 뜬 모습
마지막 스테이지 2도 클리어 Text가 떴다
잘 안보이지만 끝났다는 의미의 Game Over도 작동한다

  • 이제 마지막 게임 오버 텍스트 색상만 변경하면 스테이지 텍스트 작업을 제외한 모든 작업은 끝난다. 이제 모바일 빌드만 하면 완성이다.

5. 모바일 빌드

안드로이드로 Switch Platform해준다
Project Settings에서 Default Icon과 Company Name 등을 설정해준다
화면이 세로이어야하므로 Default Orientation을 Portrait으로 설정
Package Name을 입력하고, Scripting Backend를 IL2CPP로 변경한 뒤 ARM 64를 체크한다
빌드한다
apk 파일로 빌드되었다
Unity BE4 Game.apk
14.89MB

  • 패드가 조금 작아서 조작이 어려웠지만, 그래도 문제 없이 잘 작동한다.