기록 보관소

[Unity/유니티] 기초-2D 종스크롤 슈팅: UI 간단하게 완성하기[B31] 본문

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

[Unity/유니티] 기초-2D 종스크롤 슈팅: UI 간단하게 완성하기[B31]

JongHoon 2022. 3. 3. 21:47

개요

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

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

+) 이번 포스트는 안타깝게도 작성 중에 한번 날아가서 조금 간략하게 작성했다. 앞으로는 글 작성 중에 미리미리 저장해두는 버릇을 들어놔야겠다...

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 종스크롤 슈팅: UI 간단하게 완성하기[B31]

1. 목숨과 점수 UI 배치

UI Canvas 추가.

  • Canvas Sclaer에서 UI Scale Mode를 Scale With Screen Size로 변경.
    • Scale With Screen Size : 기준 해상도의 UI 크기 유지

Score Text 추가 및 설정. 앵커를 중앙 상단으로 잡고 좌우로 가득 채워준다.
Life Icon Image 추가 및 설정. 플레이어 라이프는 3개이므로 스프라이트도 3개 추가. 앵커는 좌측 상단으로 잡는다.
GameOver Text 추가. 화면 중앙에 앵커를 잡는다.
Retry Button 추가. 마찬가지로 중앙에 앵커를 잡는다.
Button Text 설정
GameOver Text와 Retry Button은 Over Set(Empty)에 묶어서 비활성화 시킨다.


2. UI 로직

//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

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

    public float maxSpawnDelay;
    public float curSpawnDelay;

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

        if (curSpawnDelay > maxSpawnDelay) {
            SpawnEnemy();
            maxSpawnDelay = Random.Range(0.5f, 3f);
            curSpawnDelay = 0;
        }

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

    void SpawnEnemy() {
        int ranEnemy = Random.Range(0, 3);
        int ranPoint = Random.Range(0, 9);
        GameObject enemy = Instantiate(enemyObjs[ranEnemy], spawnPoints[ranPoint].position, spawnPoints[ranPoint].rotation);
        Rigidbody2D rigid = enemy.GetComponent<Rigidbody2D>();
        Enemy enemyLogic = enemy.GetComponent<Enemy>();
        enemyLogic.player = player;

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

    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 RespawnPlayer() {
        Invoke("RespawnPlayerExe", 2f);
    }

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

    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 manager;
    public GameObject bulletObjA;
    public GameObject bulletObjB;
    Animator anim;

    public int life;
    public int score;
    public float speed;
    public float power;
    public float maxShotDelay;
    public float curShotDelay;

    public bool isTouchTop;
    public bool isTouchBottom;
    public bool isTouchLeft;
    public bool isTouchRight;

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

    void Update() {
        Move();
        Fire();
        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 = Instantiate(bulletObjA, transform.position, transform.rotation);
                Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
                rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 2:
                GameObject bulletR = Instantiate(bulletObjA, transform.position + Vector3.right * 0.1f, transform.rotation);
                GameObject bulletL = Instantiate(bulletObjA, transform.position + Vector3.left * 0.1f, transform.rotation);
                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;
            case 3:
                GameObject bulletRR = Instantiate(bulletObjA, transform.position + Vector3.right * 0.35f, transform.rotation);
                GameObject bulletCC = Instantiate(bulletObjB, transform.position, transform.rotation);
                GameObject bulletLL = Instantiate(bulletObjA, transform.position + Vector3.left * 0.35f, transform.rotation);
                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 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") {
            life--;
            manager.UpdateLifeIcon(life);

            if (life == 0) {
                manager.GameOver();
            }
            else {
                manager.RespawnPlayer();
            }
            gameObject.SetActive(false);
            Destroy(collision.gameObject);
        }

    }

    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 GameObject bulletObjA;
    public GameObject bulletObjB;
    public GameObject player;
    public Sprite[] sprites;

    SpriteRenderer spriteRenderer;

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

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

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

        if (enemyName == "S") {
            GameObject bullet = Instantiate(bulletObjA, transform.position, transform.rotation);
            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 = Instantiate(bulletObjB, transform.position + Vector3.right * 0.3f, transform.rotation);
            GameObject bulletL = Instantiate(bulletObjB, transform.position + Vector3.left * 0.3f, transform.rotation);

            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;
    }

    void onHit(int dmg) {
        health -= dmg;
        spriteRenderer.sprite = sprites[1]; //평소 스프라이트 0, 피격시 스프라이트 1
        Invoke("ReturnSprite", 0.1f);

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

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

    void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "BorderBullet") //맵의 경계로 가게되면
            Destroy(gameObject);
        else if (collision.gameObject.tag == "PlayerBullet") {    //플레이어 총알에 닿으면
            Bullet bullet = collision.gameObject.GetComponent<Bullet>();
            onHit(bullet.dmg);
            Destroy(collision.gameObject);  //플레이어 총알 삭제
        }
    }
}

GameManager 설정
Player 설정
Enemy L 설정
Enemy M 설정
Enemy S 설정
Retry Button On Click() 메소드 연결
점수가 정상적으로 쌓이고 죽으니 Game Over가 뜬 모습
Retry 버튼을 누르니 다시 초기화되어 실행되었다

  • string.format() : 지정된 양식으로 문자열을 변환해주는 함수
    • {0:n0} : 세자리마다 쉼표로 나눠주는 숫자 양식. 위 캡처에서 4,350점으로 쉼표가 찍힌것을 볼 수 있다.

하지만 동시에 2대를 맞으면
동시에 라이프가 2개가 깎이는 일이 생긴다


3. 예외 처리

//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

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

    public float maxSpawnDelay;
    public float curSpawnDelay;

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

        if (curSpawnDelay > maxSpawnDelay) {
            SpawnEnemy();
            maxSpawnDelay = Random.Range(0.5f, 3f);
            curSpawnDelay = 0;
        }

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

    void SpawnEnemy() {
        int ranEnemy = Random.Range(0, 3);
        int ranPoint = Random.Range(0, 9);
        GameObject enemy = Instantiate(enemyObjs[ranEnemy], spawnPoints[ranPoint].position, spawnPoints[ranPoint].rotation);
        Rigidbody2D rigid = enemy.GetComponent<Rigidbody2D>();
        Enemy enemyLogic = enemy.GetComponent<Enemy>();
        enemyLogic.player = player;

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

    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 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 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 manager;
    public GameObject bulletObjA;
    public GameObject bulletObjB;
    Animator anim;

    public int life;
    public int score;
    public float speed;
    public float power;
    public float maxShotDelay;
    public float curShotDelay;

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

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

    void Update() {
        Move();
        Fire();
        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 = Instantiate(bulletObjA, transform.position, transform.rotation);
                Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
                rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 2:
                GameObject bulletR = Instantiate(bulletObjA, transform.position + Vector3.right * 0.1f, transform.rotation);
                GameObject bulletL = Instantiate(bulletObjA, transform.position + Vector3.left * 0.1f, transform.rotation);
                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;
            case 3:
                GameObject bulletRR = Instantiate(bulletObjA, transform.position + Vector3.right * 0.35f, transform.rotation);
                GameObject bulletCC = Instantiate(bulletObjB, transform.position, transform.rotation);
                GameObject bulletLL = Instantiate(bulletObjA, transform.position + Vector3.left * 0.35f, transform.rotation);
                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 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 (isHit)  //이미 맞으면 다시 맞지 않도록 return
                return;

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

            if (life == 0) {
                manager.GameOver();
            }
            else {
                manager.RespawnPlayer();
            }
            gameObject.SetActive(false);
            Destroy(collision.gameObject);
        }

    }

    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;
            }
        }
    }
}

이제 동시에 2대를 맞아도
라이프가 1개만 깎인다

  • bool형 변수를 추가하여 이제 동시에 두대를 맞아 라이프가 2개 이상 깎일 일이 없어지게 되었다.