새소식

🎮 Game Dev (게임개발)/PC (데스크탑, 노트북, 터치패널)

[3D 액션게임] 17. 게임 완성하기

  • -

🔔 유튜브 크리에이터 골든메탈님의 유니티강의 3D 쿼터뷰 액션게임 [BE5] 를 보고 공부하여 작성한 게시글입니다! 🔔

 

드디어 마지막 시간이 다가왔네요, 이렇게 좋은 강의를 만들어 주신, 유튜브 골든메탈님께 감사드립니다.

Player 스크립트 전체보기

더보기
/*Player Script*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed;
    public GameObject[] weapons;
    public bool[] hasWeapons;
    public GameObject[] grenades;
    public int hasGrenades;
    public GameObject grenadeObj;
    public Camera followCamera;
    public GameManager gamemanager;

    public SoundControl soundControl;

    public int ammo;
    public int health;
    public int coin;
    public int score;

    public int maxAmmo;
    public int maxHealth;
    public int maxCoin;
    public int maxHasGrenades;

    float hAxis;
    float vAxis;

    bool rDown;
    bool jDown;
    bool fDown;
    bool gDown;
    bool reDown;
    bool iDown;
    bool sDown1;
    bool sDown2;
    bool sDown3;

    bool isJump;
    bool isDodge;
    bool isSwap;
    bool isReload = false;
    bool isFireReady = true;
    bool isBorder;
    bool isDamage;
    bool isShop;
    bool isDead;

    Vector3 moveVec;
    Vector3 dodgeVec;

    Rigidbody rigid;
    Animator anim;
    MeshRenderer[] meshs;

    GameObject nearObject;
    public Weapon equipWeapon;
    int equipWeaponIndex = -1;
    float fireDelay;

    void Awake()
    {
        rigid = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();
        meshs = GetComponentsInChildren<MeshRenderer>();

    }

    // Update is called once per frame
    void Update()
    {
        GetInput();
        Move();
        Turn();
        Jump();
        Grenade();
        Attack();
        Reload();
        Dodge();
        Swap();
        Interation();


    }


    void GetInput()
    {
        // 키보드입력에 따라 0 ~ 1로 변환 left right up down
        hAxis = Input.GetAxisRaw("Horizontal");
        vAxis = Input.GetAxisRaw("Vertical");
        rDown = Input.GetButton("Run");
        jDown = Input.GetButtonDown("Jump");
        reDown = Input.GetButtonDown("Reload");
        fDown = Input.GetButton("Fire1");
        gDown = Input.GetButtonDown("Fire2");
        iDown = Input.GetButtonDown("Interation");
        sDown1 = Input.GetButtonDown("Swap1");
        sDown2 = Input.GetButtonDown("Swap2");
        sDown3 = Input.GetButtonDown("Swap3");
    }

    void Move()
    {
        // x y z
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;

        // 회피 시 업데이트 안되게
        if (isDodge)
            moveVec = dodgeVec;

        // 스왑 및 공격 시 못움직이게
        if (isSwap || !isFireReady || isDead)
            moveVec = Vector3.zero;


        // transform + 벽뚫방지
        if (!isBorder)
            transform.position += moveVec * (rDown ? 1.3f : 1f) * speed * Time.deltaTime;

        // animator
        anim.SetBool("isWalk", moveVec != Vector3.zero);
        anim.SetBool("isRun", rDown);
    }

    void Turn()
    {
        // #1. 키보드에 의한 회전
        transform.LookAt(transform.position + moveVec);

        // #2. 마우스에 의한 회전

        // Ray란?, ScreenPointToRay() : 스크린에서 월드로 Ray를 쏘는 함수
        if (fDown && !isDead)
        {
            Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit rayHit;
            // out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
            if (Physics.Raycast(ray, out rayHit, 100))
            {
                Vector3 nextVec = rayHit.point;
                nextVec.y = 0;
                transform.LookAt(nextVec);
            }
        }
    }

    void Jump()
    {
        if (jDown && moveVec == Vector3.zero && !isJump && !isDodge && !isDead) {
            // 물리엔진에 힘을 준다. 여기선 즉발적인 Impulse
            rigid.AddForce(Vector3.up * 15, ForceMode.Impulse);
            anim.SetBool("isJump", true);
            anim.SetTrigger("doJump");

            isJump = true;

            soundControl.jumpSound.Play();
        }
    }

    void Grenade()
    {
        if (hasGrenades == 0)
            return;

        if (gDown && !isReload && !isSwap && !isDead) {

            Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit rayHit;
            // out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
            if (Physics.Raycast(ray, out rayHit, 100))
            {

                Vector3 nextVec = rayHit.point;
                nextVec.y = 15;

                GameObject instantGrenade = Instantiate(grenadeObj, transform.position, transform.rotation);
                Rigidbody rigidGrenade = instantGrenade.GetComponent<Rigidbody>();
                rigidGrenade.AddForce(nextVec, ForceMode.Impulse);
                rigidGrenade.AddTorque(Vector3.back * 10, ForceMode.Impulse);

                hasGrenades--;
                grenades[hasGrenades].SetActive(false);
            }

            soundControl.ThrowSound.Play();
        }
    }

    void Attack()
    {
        // 공격할 조건만 플레이어에 두고, 공격로직은 무기에 위임한다.
        if (equipWeapon == null)
            return;

        fireDelay += Time.deltaTime;
        isFireReady = equipWeapon.rate < fireDelay;

        if (fDown && isFireReady && !isDodge && !isSwap && !isShop && !isDead) {
            equipWeapon.Use();
            anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
            fireDelay = 0;
        }
    }

    void Reload()
    {
        if (equipWeapon == null)
            return;
        if (equipWeapon.type == Weapon.Type.Melee)
            return;
        if (ammo == 0 || equipWeapon.curAmmo == equipWeapon.maxAmmo)
            return;

        if (reDown && !isJump && !isDodge && !isSwap && isFireReady && !isShop && !isDead)
        {
            anim.SetTrigger("doReload");
            isReload = true;

            // Invoke("ReloadOut", 2f); -> 애니메이션 기능에 ReloadOut() 기능 구현했습니다.
        }
    }

    public void ReloadOut()
    {
        int reAmmo = equipWeapon.maxAmmo - equipWeapon.curAmmo;
        reAmmo = ammo < reAmmo ? ammo : reAmmo;

        equipWeapon.curAmmo += reAmmo;
        ammo -= reAmmo;
        isReload = false;
    }

    void Dodge()
    {
        // 이동하면서 점프할때, 
        if (jDown && moveVec != Vector3.zero && !isJump && !isDodge && !isShop && !isDead)
        {
            dodgeVec = moveVec;
            speed *= 2;
            anim.SetTrigger("doDodge");
            isDodge = true;

            Invoke("DodgeOut", 0.5f); // 시간지연 라이브러리 기능
            soundControl.dodgeSound.Play();
        }
    }

    void DodgeOut()
    {
        speed *= 0.5f;
        isDodge = false;
    }

    void Swap()
    {
        if (sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))
            return;
        if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))
            return;
        if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))
            return;

        int weaponIndex = -1;
        if (sDown1) weaponIndex = 0;
        if (sDown2) weaponIndex = 1;
        if (sDown3) weaponIndex = 2;

        // 1, 2 ,3 버튼 누를때
        if ((sDown1 || sDown2 || sDown3) && !isJump && !isDodge && !isShop && !isDead) {
            if (equipWeapon != null)
                equipWeapon.gameObject.SetActive(false);

            equipWeaponIndex = weaponIndex;
            equipWeapon = weapons[weaponIndex].GetComponent<Weapon>();
            equipWeapon.gameObject.SetActive(true);

            anim.SetTrigger("doSwap");


            // 스왑 중
            isSwap = true;
            Invoke("SwapOut", 0.4f);

        }
    }
    void SwapOut()
    {
        isSwap = false;
    }

    void Interation()
    {
        if (iDown && nearObject != null && !isJump && !isDodge && !isDead) {
            if (nearObject.tag == "Weapon") {
                Item item = nearObject.GetComponent<Item>();
                int weaponIndex = item.value;
                hasWeapons[weaponIndex] = true;

                Destroy(nearObject);
            }
            else if (nearObject.tag == "Shop")
            {
                Shop shop = nearObject.GetComponent<Shop>();
                shop.Enter(this);
                isShop = true;
            }
            soundControl.InteractSound.Play();
        }
        
    }

    void FreezeRotation()
    {
        rigid.angularVelocity = Vector3.zero;

    }

    void StopToWall()
    {
        // DrawRay() : Scene내에 Ray를 보여주는 함수
        //Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
        isBorder = Physics.Raycast(transform.position, transform.forward, 3, LayerMask.GetMask("Wall"));
    }

    void FixedUpdate()
    {
        FreezeRotation();
        StopToWall();
    }

    // 충돌 시 이벤트 함수로 착지 구현
    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "Floor") {
            isJump = false;
            anim.SetBool("isJump", false);
        }
    }

    // 아이템 획득
    private void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Item") {
            Item item = other.GetComponent<Item>();
            switch (item.type) {
                case Item.Type.Ammo:
                    ammo += item.value;
                    if (ammo > maxAmmo)
                        ammo = maxAmmo;
                    break;
                case Item.Type.Coin:
                    coin += item.value;
                    if (coin > maxCoin)
                        coin = maxCoin;
                    break;
                case Item.Type.Heart:
                    health += item.value;
                    if (health > maxHealth)
                        health = maxHealth;
                    break;
                case Item.Type.Grenade:
                    if (hasGrenades == maxHasGrenades)
                        return;
                    grenades[hasGrenades].SetActive(true);
                    hasGrenades += item.value;
                    break;
            }
            Destroy(other.gameObject);
            soundControl.InteractSound.Play();
        }
        else if (other.tag == "EnemyBullet") {
            if (!isDamage) {
                Bullet enemyBullet = other.GetComponent<Bullet>();
                health -= enemyBullet.damage;

                bool isBossAtk = other.name == "Boss Melee Area";
                StartCoroutine(OnDamage(isBossAtk));
            }

            if (other.GetComponent<Rigidbody>() != null)
                Destroy(other.gameObject);

            soundControl.playerOnDamageSound.Play();
        }


    }

    IEnumerator OnDamage(bool isBossAtk)
    {
        isDamage = true;
        foreach (MeshRenderer mesh in meshs) {
            mesh.material.color = Color.yellow;
        }

        if (isBossAtk)
            rigid.AddForce(transform.forward * -25, ForceMode.Impulse);

        if (health <= 0 && !isDead)
            OnDie();

        yield return new WaitForSeconds(1f);

        isDamage = false;
        foreach (MeshRenderer mesh in meshs)
        {
            mesh.material.color = Color.white;
        }

        if (isBossAtk)
            rigid.velocity = Vector3.zero;



    }

    void OnDie()
    {
        anim.SetTrigger("doDie");
        isDead = true;
        gamemanager.GameOver();
    }

    private void OnTriggerStay(Collider other)
    {
        if (other.tag == "Weapon" || other.tag == "Shop")
            nearObject = other.gameObject;
    }
    private void OnTriggerExit(Collider other)
    {
        if (other.tag == "Weapon")
            nearObject = null;
        else if (other.tag == "Shop")
        {
            Shop shop = nearObject.GetComponent<Shop>();
            shop.Exit();
            isShop = false;
            nearObject = null;
        }
    }
}

GameManager 스크립트 전체보기

더보기
/*GameObject Script*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
    public GameObject menuCam;
    public GameObject gameCam;
    public Player player;
    public Boss boss;
    public GameObject itemShop;
    public GameObject weaponShop;
    public GameObject startZone;
    public int stage;
    public float playTime;
    public bool isBattle;
    public int enemyCntA;
    public int enemyCntB;
    public int enemyCntC;
    public int enemyCntD;

    public Transform[] enemyZones;
    public GameObject[] enemies;
    public List<int> enemyList;

    public GameObject menuPanel;
    public GameObject gamePanel;
    public GameObject overPanel;
    public Text maxScoreTxt;
    public Text scoreTxt;
    public Text stageTxt;
    public Text playerTimeTxt;
    public Text playerHealthTxt;
    public Text playerAmmoTxt;
    public Text playerCoinTxt;
    public Image weapon1Img;
    public Image weapon2Img;
    public Image weapon3Img;
    public Image weaponRImg;
    public Text enemyATxt;
    public Text enemyBTxt;
    public Text enemyCTxt;
    public RectTransform bossHealthGroup;
    public RectTransform bossHealthBar;
    public Text curScoreText;
    public Text bestText;
    void Awake()
    {
        enemyList = new List<int>();
        maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore"));

        if (PlayerPrefs.HasKey("MaxScore"))
            PlayerPrefs.SetInt("MaxScore", 0);
    }

    public void GameStart()
    {
        menuCam.SetActive(false);
        gameCam.SetActive(true);

        menuPanel.SetActive(false);
        gamePanel.SetActive(true);

        player.gameObject.SetActive(true);
    }
     public void GameOver()
    {
        gamePanel.SetActive(false);
        overPanel.SetActive(true);
        curScoreText.text = scoreTxt.text;

        int maxScore = PlayerPrefs.GetInt("MaxScore");
        if(player.score > maxScore) {
            bestText.gameObject.SetActive(true);
            PlayerPrefs.SetInt("MaxScore", player.score);
        }
    }

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

    public void StageStart()
    {
        isBattle = true;
        itemShop.SetActive(false);
        weaponShop.SetActive(false);
        startZone.SetActive(false);

        player.transform.position = Vector3.up * 0.8f;
        foreach (Transform zone in enemyZones)
            zone.gameObject.SetActive(true);

        StartCoroutine(InBattle());

    }
    
    public void StageEnd()
    {
        isBattle = false;
        player.transform.position = Vector3.up * 0.8f;
        itemShop.SetActive(true);
        weaponShop.SetActive(true);
        startZone.SetActive(true);

        foreach (Transform zone in enemyZones)
            zone.gameObject.SetActive(false);


        stage++;
    }

    IEnumerator InBattle()
    {
        if(stage % 5 == 0) {
            
            enemyCntD++;
            GameObject instantEnemy = Instantiate(enemies[3], enemyZones[3].position, enemyZones[3].rotation);
            Enemy enemy = instantEnemy.GetComponent<Enemy>();
            enemy.Target = player.transform;
            enemy.manager = this;
            boss = instantEnemy.GetComponent<Boss>();
            

        }
        else{

            for (int index = 0; index < stage; index++)
            {
                int ran = Random.Range(0, 3);
                enemyList.Add(ran);

                switch (ran)
                {
                    case 0:
                        enemyCntA++;
                        break;
                    case 1:
                        enemyCntB++;
                        break;
                    case 2:
                        enemyCntC++;
                        break;
                }
            }

            while (enemyList.Count > 0)
            {
                int ranZone = Random.Range(0, 4);
                GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
                Enemy enemy = instantEnemy.GetComponent<Enemy>();
                enemy.Target = player.transform;
                enemy.manager = this;
                enemyList.RemoveAt(0);
                yield return new WaitForSeconds(4f);
            }
        }

        while (enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
            yield return null;
        }

        yield return new WaitForSeconds(4f);
        boss = null;
        StageEnd();
    }

    void Update()
    {
        if (isBattle)
            playTime += Time.deltaTime;
        
    }

    void LateUpdate() //Update 끝난 후 호출되는 생명주기
    {
        // 상단 UI
        scoreTxt.text = string.Format("{0:n0}", player.score);
        stageTxt.text = "STAGE " + stage;

        int hour = (int)(playTime / 3600);
        int min = (int)((playTime - hour * 3600) / 60);
        int sec = (int)(playTime % 60);
        playerTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":"+ string.Format("{0:00}", sec);

        // 플레이어 UI
        playerHealthTxt.text = player.health + " / " + player.maxHealth;
        playerCoinTxt.text = string.Format("{0:n0}", player.coin);
        if (player.equipWeapon == null)
            playerAmmoTxt.text = "- / " + player.ammo;
        else if (player.equipWeapon.type == Weapon.Type.Melee)
            playerAmmoTxt.text = "- / " + player.ammo;
        else 
            playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;

        // 무기 UI
        weapon1Img.color = new Color(1, 1, 1, player.hasWeapons[0] ? 1 : 0.3f);
        weapon2Img.color = new Color(1, 1, 1, player.hasWeapons[1] ? 1 : 0.3f);
        weapon3Img.color = new Color(1, 1, 1, player.hasWeapons[2] ? 1 : 0.3f);
        weaponRImg.color = new Color(1, 1, 1, player.hasGrenades > 0 ? 1 : 0.3f);

        // 몬스터 숫자 UI
        enemyATxt.text = "x " + enemyCntA.ToString();
        enemyBTxt.text = "x " + enemyCntB.ToString();
        enemyCTxt.text = "x " + enemyCntC.ToString();

        // 보스 체력 UI
        if (boss != null)
        {
            bossHealthGroup.anchoredPosition = Vector3.down * 30;
            bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
        }
        else {
            bossHealthGroup.anchoredPosition = Vector3.up * 300;
        }
    }
}

Enemy 스크립트 전체보기

더보기
/*Enemy Script*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour
{
    public enum Type { A, B, C, D };
    public Type enemyType;
    public int maxHealth;
    public int curHealth;
    public int score;
    public GameManager manager;
    public Transform Target;
    public BoxCollider melleArea;
    public GameObject bullet;
    public GameObject[] coins;

    public bool isChase;
    public bool isAttack;
    public bool isDead;
    
    public Rigidbody rigid;
    public BoxCollider boxCollider;
    public MeshRenderer[] meshs;
    public NavMeshAgent nav;
    public Animator anim;

    void Awake()
    {
        rigid = GetComponent<Rigidbody>();
        boxCollider = GetComponent<BoxCollider>();

        // Material은 Mesh Renderer 컴포넌트에서 접근가능합니다.
        meshs = GetComponentsInChildren<MeshRenderer>();
        // 네비게이션
        nav = GetComponent<NavMeshAgent>();
        //애니메이션
        anim = GetComponentInChildren<Animator>();

        if(enemyType != Type.D)
            Invoke("ChaseStart", 2);

    }

    void ChaseStart()
    {
        isChase = true;
        anim.SetBool("isWalk", true);

    }

    void Update()
    {
        if (nav.enabled&& enemyType != Type.D) {
            nav.SetDestination(Target.position);
            nav.isStopped = !isChase;
        }
        
    }

    void FreezeVelocity()
    {
        if (isChase) {
            rigid.velocity = Vector3.zero;
            rigid.angularVelocity = Vector3.zero;
        }
    }

    void Targeting()
    {
        if (!isDead && enemyType != Type.D)
        {
            float targetRadius = 0;
            float targetRange = 0;

            switch (enemyType)
            {
                case Type.A:
                    targetRadius = 1.5f;
                    targetRange = 3f;
                    break;

                case Type.B:
                    targetRadius = 1f;
                    targetRange = 12f;
                    break;

                case Type.C:
                    targetRadius = 0.5f;
                    targetRange = 25f;
                    break;
            }


            RaycastHit[] rayHits =
                Physics.SphereCastAll(transform.position,
                                       targetRadius,
                                       transform.forward,
                                       targetRange,
                                       LayerMask.GetMask("Player"));

            if (rayHits.Length > 0 && !isAttack)
            {
                StartCoroutine(Attack());
            }

        }
    }

    IEnumerator Attack()
    {
        isChase = false;
        isAttack = true;
        anim.SetBool("isAttack", true);

        switch (enemyType) {
            case Type.A:
                yield return new WaitForSeconds(0.2f);
                melleArea.enabled = true;

                yield return new WaitForSeconds(1f);
                melleArea.enabled = false;
                break;

            case Type.B:
                yield return new WaitForSeconds(0.1f);
                rigid.AddForce(transform.forward * 20, ForceMode.Impulse);
                melleArea.enabled = true;

                yield return new WaitForSeconds(0.5f);
                rigid.velocity = Vector3.zero;
                melleArea.enabled = false;

                yield return new WaitForSeconds(2f);
                break;

            case Type.C:
                yield return new WaitForSeconds(0.5f);
                GameObject instantBullet = Instantiate(bullet, transform.position, transform.rotation);
                Rigidbody rigidBullet = instantBullet.GetComponent<Rigidbody>();
                rigidBullet.velocity = transform.forward * 20;

                yield return new WaitForSeconds(2f);
                break;
        }
        
        isChase = true;
        isAttack = false;
        anim.SetBool("isAttack", false);
    }

    void FixedUpdate()
    {
        Targeting();
        FreezeVelocity();
    }

    void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Melee"){
            Weapon weapon = other.GetComponent<Weapon>();
            curHealth -= weapon.damage;
            Vector3 reactVec = transform.position - other.transform.position;

            StartCoroutine(OnDamage(reactVec, false));
        }
        else if(other.tag == "Bullet"){
            Bullet bullet = other.GetComponent<Bullet>();
            curHealth -= bullet.damage;
            Vector3 reactVec = transform.position - other.transform.position;

            Destroy(other.gameObject);

            StartCoroutine(OnDamage(reactVec, false));
        }
    }

            
    public void HitByGrenade(Vector3 explosionPos)
    {
        curHealth -= 100;
        Vector3 reactVec = transform.position - explosionPos;
        StartCoroutine(OnDamage(reactVec, true));
    }

    IEnumerator OnDamage(Vector3 reactVec, bool isGrenade)
    {
        foreach (MeshRenderer mesh in meshs)
            mesh.material.color = Color.red;
        
        yield return new WaitForSeconds(0.1f);

        if(curHealth > 0) {
            foreach (MeshRenderer mesh in meshs)
                mesh.material.color = Color.white;
            
        }
        else {
            if (!isDead)
            {
                foreach (MeshRenderer mesh in meshs)
                    mesh.material.color = Color.gray;


                gameObject.layer = 12;
                isDead = true;
                isChase = false;
                nav.enabled = false;
                anim.SetTrigger("doDie");
                Player player = Target.GetComponent<Player>();
                player.score += score;

                int ranCoin = Random.Range(0, 3);
                Instantiate(coins[ranCoin], transform.position, Quaternion.identity);
                yield return new WaitForSeconds(0.1f);


                switch (enemyType)
                {
                    case Type.A:
                        manager.enemyCntA--;
                        break;
                    case Type.B:
                        manager.enemyCntB--;
                        break;
                    case Type.C:
                        manager.enemyCntC--;
                        break;
                    case Type.D:
                        manager.enemyCntD--;
                        break;
                }
            }


            // 죽었을 시, 넉백

            if (isGrenade)
            {
                reactVec = reactVec.normalized;
                reactVec += Vector3.up * 3;

                rigid.freezeRotation = false; // freezeRotation
                rigid.AddForce(reactVec * 5, ForceMode.Impulse);
                rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
            }
            else
            {
                reactVec = reactVec.normalized;
                reactVec += Vector3.up;
                rigid.AddForce(reactVec * 5, ForceMode.Impulse);
            }
            
            
            Destroy(gameObject, 4);
        }
    }
}

 

🧷 1. 스테이지 시작 및 종료

- Start Zone 오브젝트

  • 전투를 하고, 스테이지를 플레이 하고 싶을 때, 이 안으로 들어가면 시작되는 zone입니다.
  • 상점의 아무 zone을 복사 붙혀넣기 한 후, 속에 3D TEXT를 넣어줍니다.

- Monster Respawn Zone 오브젝트

  • 파티클만 들어간 몬스터 리스폰 오브젝트를 만듭니다.

 

- Game Manager 스테이지 시작, 종료기능

public void StageStart()
{
    isBattle = true;
    itemShop.SetActive(false);
    weaponShop.SetActive(false);
    startZone.SetActive(false);

    player.transform.position = Vector3.up * 0.8f;
    foreach (Transform zone in enemyZones)
        zone.gameObject.SetActive(true);

    StartCoroutine(InBattle());

}

public void StageEnd()
{
    isBattle = false;
    player.transform.position = Vector3.up * 0.8f;
    itemShop.SetActive(true);
    weaponShop.SetActive(true);
    startZone.SetActive(true);

    foreach (Transform zone in enemyZones)
        zone.gameObject.SetActive(false);


    stage++;
}
  • 게임이 시작되면, 상점과 start zone 은 비활성화 됩니다. enemyZone은 활성화 됩니다.
  • 플레이어의 위치는 중앙으로 바뀌게 됩니다. 이후 InBattle() 코루틴이 시작됩니다.
  • 게임이 종료되면 플레이어 위치는 중앙에 옵니다.
  • 게임이 종료되면 상점과 start zone은 다시 활성화가 됩니다. enemyZone은 비활성화 됩니다.

 

- Start zone script

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

public class StratZone : MonoBehaviour
{
    public GameManager manager;

    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "Player")
            manager.StageStart();
    }
}
  • start zone에 ontriggerEnter 메서드를 두어서, 플레이어가 닿을시에 StageStart() 함수가 일어나게 합니다.

 

🧷 2. 스테이지 시작 후 배틀 시작

- InBattle() 함수

IEnumerator InBattle()
{
    if(stage % 5 == 0) {

        enemyCntD++;
        GameObject instantEnemy = Instantiate(enemies[3], enemyZones[3].position, enemyZones[3].rotation);
        Enemy enemy = instantEnemy.GetComponent<Enemy>();
        enemy.Target = player.transform;
        enemy.manager = this;
        boss = instantEnemy.GetComponent<Boss>();


    }
    else{

        for (int index = 0; index < stage; index++)
        {
            int ran = Random.Range(0, 3);
            enemyList.Add(ran);

            switch (ran)
            {
                case 0:
                    enemyCntA++;
                    break;
                case 1:
                    enemyCntB++;
                    break;
                case 2:
                    enemyCntC++;
                    break;
            }
        }

        while (enemyList.Count > 0)
        {
            int ranZone = Random.Range(0, 4);
            GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
            Enemy enemy = instantEnemy.GetComponent<Enemy>();
            enemy.Target = player.transform;
            enemy.manager = this;
            enemyList.RemoveAt(0);
            yield return new WaitForSeconds(4f);
        }
    }

    while (enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
        yield return null;
    }

    yield return new WaitForSeconds(4f);
    boss = null;
    StageEnd();
}
  • 코루틴으로 이루어지고, List에 몬스터 index를 저장합니다.
  • 새로 태어난 기본 인스턴트 몬스터들은, target을 붙혀주고 manager도 붙혀줍니다.
  • 기본 몬스터들은 리스트의 0번째 인덱스부터 게임 속 에 나오며, 리스트를 제거해가며 출현시켜줍니다.
  • 그리고 적의 수를 세기위해서 enemy 스크립트에도 game manager을 붙혀서 죽었을시, 카운터를 줄여주는 코드도 넣어야합니다.
  • 코루틴으로 이후에 적이 다 없을 때 까지, null로 막아주다가, 적의 수가 0이되고 4초후에 StageEnd()를 호출합니다.
  • 5스테이지마다 보스는 따로 넣어줍니다. + BOSS 스크립트, target, manager 등등

 

🧷 3. 게임 오버 후, 게임 재시작

- 게임오버 UI

  • 게임 오버 UI를 만들어줍니다
  • Cur Score Image
  • Cur Score Text
  • ReStart Button
  • Best Scroe Text
  • Game Over Text
  • 순으로 꾸며줄 예정입니다.

 

 

- 플레이어 죽을 시 게임오버 UI 연결

Enumerator OnDamage(bool isBossAtk)
    {
        if (health <= 0 && !isDead)
            OnDie();
            
            // yield return null;
    }

void OnDie()
{
    anim.SetTrigger("doDie");
    isDead = true;
    gamemanager.GameOver();
}
  • 플레이어 스크립트에, 죽을 시 OnDie() 함수를 호출하여 Gameover() 함수도 호출해줍니다.

-  게임 재시작 연결

Restart Button

 

Restart()

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

-  게임 점수

void Awake()
{
    if (PlayerPrefs.HasKey("MaxScore"))
        PlayerPrefs.SetInt("MaxScore", 0);
}


 public void GameOver()
{
    gamePanel.SetActive(false);
    overPanel.SetActive(true);
    curScoreText.text = scoreTxt.text;

    int maxScore = PlayerPrefs.GetInt("MaxScore");
    if(player.score > maxScore) {
        bestText.gameObject.SetActive(true);
        PlayerPrefs.SetInt("MaxScore", player.score);
    }
}
  • 게임 시작 시에 MaxScroe을 세팅해줍니다. (Key가 없다면 새로운 최대 점수를 만들어 주는 것입니다.)
  • 플레이어 점수는 저장되어 최댓값만 넣어주는 코드입니다.

 

-  Game Manager 오브젝트 연결

오브젝트 연결을 잘해줘야합니다.

 

 

이후 여러 버그들을 고치고, 입맛대로 게임을 꾸민 후 빌드해주면 끝입니다.!!

 

저는 망치로 때릴 때, 몬스터의 수가 -가 되버리더라구요 그래서, enemy스크립트에 isDead 플래그를 세워, 죽는게 1번만 돌아가도록 하였습니다.


++ 이제 추가적으로, 난이도 조정과 음향을 맞게 넣으면 됩니다.

그리고 배경과 지형도 재밌게 꾸며보고 싶습니다.

이후에 3D 카메라기능을 설정한 후 생동감있는 fps 느낌도 내어볼 예정입니다.

추가적으로 세가지의 무기를 기본으로 업그레이드를 해가고 싶습니다.++

 

이를 이용하여 네트워크상에서 친구의 플레이어도 만나보고 싶네요.

 

 

 

 

 

 

 

 

 

 

출처: 골든메탈님 유튜브

https://www.youtube.com/watch?v=9g4prUqF2oA&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy&index=17 

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.