새소식

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

[3D 액션게임] 13. 보스 만들기

  • -

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

 

플레이어 스크립트 전체보기

더보기
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 int ammo;
    public int health;
    public int coin;
    
    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;

    Vector3 moveVec;
    Vector3 dodgeVec;
    
    Rigidbody rigid;
    Animator anim;
    MeshRenderer[] meshs;

    GameObject nearObject;
    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)
            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)
        {
            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) {
            // 물리엔진에 힘을 준다. 여기선 즉발적인 Impulse
            rigid.AddForce(Vector3.up * 15, ForceMode.Impulse);
            anim.SetBool("isJump", true);
            anim.SetTrigger("doJump");

            isJump = true;
        }
    }

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

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

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

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

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

        if(fDown && isFireReady && !isDodge && !isSwap) {
            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)
        {
            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)
        {
            dodgeVec = moveVec;
            speed *= 2;
            anim.SetTrigger("doDodge");
            isDodge = true;

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

    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) {
            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) {
            if(nearObject.tag == "Weapon") {
                Item item = nearObject.GetComponent<Item>();
                int weaponIndex = item.value;
                hasWeapons[weaponIndex] = true;

                Destroy(nearObject);
            }
        }
    }

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


    }

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

        yield return new WaitForSeconds(1f);

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

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


    }


    private void OnTriggerStay(Collider other)
    {
        if (other.tag == "Weapon")
            nearObject = other.gameObject;
    }
    private void OnTriggerExit(Collider other)
    {
        if (other.tag == "Weapon")
            nearObject = null;
    }
}

적 스크립트 전체보기

더보기
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 Transform Target;
    public BoxCollider melleArea;
    public GameObject bullet;
    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 {
            foreach (MeshRenderer mesh in meshs)
                mesh.material.color = Color.gray;
            
            
            gameObject.layer = 12;
            isDead = true;
            isChase = false;
            nav.enabled = false;
            anim.SetTrigger("doDie");


            // 죽었을 시, 넉백

            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);
            }
            
            if(enemyType != Type.D)
                Destroy(gameObject, 4);
        }
    }
}

보스 스크립트 전체보기

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Boss : Enemy
{
    public GameObject missile;
    public Transform missilePortA;
    public Transform missilePortB;

    Vector3 lookVec; // 플레이어 움직임 예측
    Vector3 tauntVec; // 찍어내리는 벡터
    public bool isLook; // 플레이어 바라보는 플래그

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

        nav.isStopped = true;
        StartCoroutine(Think());


    }

    
    void Update()
    {
        if (isDead){
            StopAllCoroutines();
            return;
        }

        if (isLook)
        {
            float h = Input.GetAxisRaw("Horizontal");
            float v = Input.GetAxisRaw("Vertical");
            lookVec = new Vector3(h, 0, v) * 5f;
            transform.LookAt(Target.position + lookVec);
        }
        else
            nav.SetDestination(tauntVec);
    }
     
    IEnumerator Think()
    {
        yield return new WaitForSeconds(0.1f);

        int ranAction = Random.Range(0, 5); // 0 ~ 4

        switch (ranAction)
        {
            case 0:
            case 1:
                // 미사일 발사 패턴
                StartCoroutine(MissileShot());
                break;
            case 2:
            case 3:
                // 돌 굴러가는 패턴
                StartCoroutine(RockShot());
                break;
            case 4:
                // 점프 공격 패턴
                StartCoroutine(Taunt());
                break;

        }
    }

    IEnumerator MissileShot() 
    {
        anim.SetTrigger("doShot");
        yield return new WaitForSeconds(0.2f);
        GameObject instantMissileA = Instantiate(missile, missilePortA.position, missilePortA.rotation);
        BossMissile bossMissileA = instantMissileA.GetComponent<BossMissile>();
        bossMissileA.target = Target;

        yield return new WaitForSeconds(0.3f);
        GameObject instantMissileB = Instantiate(missile, missilePortB.position, missilePortB.rotation);
        BossMissile bossMissileB = instantMissileB.GetComponent<BossMissile>();
        bossMissileB.target = Target;

        yield return new WaitForSeconds(2.5f);

        StartCoroutine(Think());
    }
    IEnumerator RockShot()
    {
        isLook = false;
        anim.SetTrigger("doBigShot");

        Instantiate(bullet, transform.position, transform.rotation);
        yield return new WaitForSeconds(3f);

        isLook = true;
        StartCoroutine(Think());
    }
    IEnumerator Taunt() 
    {
        tauntVec = Target.position + lookVec;
        isLook = false;
        nav.isStopped = false;
        boxCollider.enabled = false;
        anim.SetTrigger("doTaunt");

        yield return new WaitForSeconds(1.5f);
        melleArea.enabled = true;

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

        yield return new WaitForSeconds(1f);
        isLook = true;
        nav.isStopped = true;
        boxCollider.enabled = true;

        StartCoroutine(Think());
    }

}

🧷 1. 보스 오브젝트 및 애니메이션

-  보스 오브젝트 설정

  • rigidbody 추가후, 쓰러짐 방지를 위해 Freeze Rotation X,Z 체크
  • Box Collider 추가후, 콜라이더 영역 맞추기
  • Nav Mesh Agent 추가, Angular Speed는 0으로 두었습니다.

 

-  보스 애니메이션 설정

  • Enemy D 라는 새로운 애니메이션 컨트롤러 추가 후, 보스 오브젝트 하위 Mesh Object에 추가하였습니다.
  • 공격 패턴인 BigShot, Shot, Taunt와 죽는 모션인 Die는 트리거로 설정합니다.
  • Any State에서 애니메이션으로 가는 트랜잭션은 Has Exit Time을 체크해제하고, Transition Duration을 0으로 둡니다
  • 애니메이션에서 Exit로 가는 트랜잭션은 Has Exit Time을 체크해 줍시다. Transition Duration은 0.1초로 설정합니다.

 

-  미사일 발사 위치

  • 보스 공격패턴중 하나인 미사일 발사 위치 A, B를 위치에 맞게 양 귀쪽으로 설정해줍시다.

 

-  근접 공격 영역

  • 보스 공격패턴중 점프해서 떨어지는 근접공격 패턴은, Box Collider와, Bullet 스크립트를 추가해 영역을 줍니다.

 

🧷 2. 미사일, 돌 공격 오브젝트

-  미사일 오브젝트

  • 에셋의 프리팹에서 Missile Boss 프리팹을 가져옵니다.
  • Effect로 Particle System을 주어 불꽃을 표현해 주었습니다.
  • 여기서 중요한 점은 오브젝트의 forword 방향이 z축이기에 그에 맞게 Mesh Object를 회전시켜줍시다.
  • Mesh Object에 Missile 스크립트를 장착시켜, 회전감을 줍니다.
  • 부모인 Object안 컴포넌트로는 Box Collider인 피격 영역 , Nav Mesh Agent를 통한 유도기능을 추가합니다.
    • Box Collider은 플레이어를 공격하고 사라지기에 isTrigger을 체크해줍시다.
    • Nav Mesh Agent는 속도감을 위하여, Speed와, Agular Speed, Acceleration등을 입맛대로 설정합니다.

 

이후 Boss Missile 이라는 Bullet에서 상속받는 스크립트를 추가해줍니다.

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

public class BossMissile : Bullet
{

    public Transform target;
    NavMeshAgent nav;


    void Awake()
    {
        nav = GetComponent<NavMeshAgent>();
    }

    
    void Update()
    {
        nav.SetDestination(target.position);
    }
}
  • 보스 미사일에는 유도기능이 장착되어 있습니다.

 

-  돌 오브젝트

  • 미사일 처럼 에셋에서 Boss Rock을 가져와, 컴포넌트를 추가합니다.
  • RigidBody : Mass 10, Freez Rotation Y, Z (굴러서 공격하여, X축은 회전할 예정입니다.)
  • Sphere Collider 한개는 물리효과를 위해, 하나는 플레이어 피격을 위한 is Trigger을 체크합니다.

이후 Boss Rock이라는 Bullet을 상속받은 스크립트를 추가합니다.

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

public class BossRock : Bullet
{
    Rigidbody rigid;
    float angularPower = 2f;
    float scaleValue = 0.1f;
    bool isShoot;

    void Awake()
    {
        rigid = GetComponent<Rigidbody>();
        StartCoroutine(GainPowerTimer());
        StartCoroutine(GainPower());
    }

    IEnumerator GainPowerTimer()
    {
        yield return new WaitForSeconds(2.2f);
        isShoot = true;
    }

    IEnumerator GainPower()
    {
        while (!isShoot) {
            angularPower += 0.02f;
            scaleValue += 0.005f;
            transform.localScale = Vector3.one * scaleValue;
            rigid.AddTorque(transform.right * angularPower, ForceMode.Acceleration);

            yield return null;

        }
    }

}
  • 코루틴을 이용한 GainPowerTimer() 함수는 돌이 발사하기까지 기를 모으는 함수입니다. 
    • 2.2초의 시간동안 모을 수 있씁니다.
    • isShoot 플래그로 돌맹이의 발사준비를 알립니다.
  • GainPower() 함수는 isShoot 플래그가 false인 경우에 지속적으로 회전력과, 크기를 키워줍니다.

 

public class Bullet : MonoBehaviour
{
    public int damage;
    public bool isMelee;
    public bool isRock;

    void OnCollisionEnter(Collision collision)
    {
        if(!isRock && collision.gameObject.tag == "Floor") {
            Destroy(gameObject, 3);
        }
        
    }

    void OnTriggerEnter(Collider other)
    {
        if (!isMelee && other.gameObject.tag == "Wall")
        {
            Destroy(gameObject);
        }
    }
}
  • Bullet 스크립트에는, isRock 플래그를 두어, 돌이 바닥에 닿더라도 사라지지 않게하는 조건을 추가합니다.

 

🧷 3. 보스 기본로직

보스는 기본적으로 점프공격빼고 그 자리에 가만히 있습니다.

그리고 플레이어를 맞추기 쉽게 하기위해, 플레이어 움직임을 예측하는 AI 회전기능을 만듭니다.

-  Boss 스크립트 상속을 위한 Enemy 스크립트 로직 정리 1

/*Enemy script, Boss 상속을 위해 고쳐줍시다. 수정된 부분만 담긴 스크립트 일부입니다.*/
public class Enemy : MonoBehaviour
{
    public Rigidbody rigid;
    public BoxCollider boxCollider;
    public MeshRenderer[] meshs;
    public NavMeshAgent nav;
    public Animator anim;
}
  • 보스 스트립트에서 사용하기 위해, public으로 바꿔 줍니다.

 

-  Boss 스크립트 상속을 위한 Enemy 스크립트 로직 정리 2

/*Enemy script, Boss 상속을 위해 고쳐줍시다. 수정된 부분만 담긴 스크립트 일부입니다.*/
public class Enemy : MonoBehaviour
{
    public enum Type { A, B, C, D };

    public bool isDead;

    void Awake()
    {
        if(enemyType != Type.D)
            Invoke("ChaseStart", 2);
    }

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

    void Targeting()
    {
    	// 타게팅은 D가 아닐때, 몬스터가 죽지 않았을 때 를 기준으로 해줍니다.
        if (!isDead && enemyType != Type.D) {}
    }


    IEnumerator OnDamage(Vector3 reactVec, bool isGrenade)
    {
        if(curHealth > 0) {      
        }
        else {

            // 죽었을 시
            if(enemyType != Type.D)
                Destroy(gameObject, 4);
        }
    }
}
  • ChaseStart() 기능은 보스가 아닌 경우에만 실행
  • Update() 구문의 navigate 설정부분도, 보스가 아닌 경우에만
  • Targeting() 구문도, 보스가 아닌 경우에
  • OnDamage()에서 몬스터가 파괴되는 경우도, 보스는 파괴되지 않게 해줍시다.

 

-  Enemy 피격 시 색변경 로직 살짝 바꿔주기

/*Enemy script, 수정된 부분만 담긴 스크립트 일부입니다.*/
public class Enemy : MonoBehaviour
{
    public MeshRenderer[] meshs;
    
    void Awake()
    {
        // Material은 Mesh Renderer 컴포넌트에서 접근가능합니다.
        meshs = GetComponentsInChildren<MeshRenderer>();
    }
    
    IEnumerator OnDamage(Vector3 reactVec, bool isGrenade)
    {
        foreach (MeshRenderer mesh in meshs)
            mesh.material.color = Color.red;

        if(curHealth > 0) {
            foreach (MeshRenderer mesh in meshs)
                mesh.material.color = Color.white;           
        }
        else {
            foreach (MeshRenderer mesh in meshs)
                mesh.material.color = Color.gray;
        }
    }
}
  • forEach 문을 사용하여 적 피격시 모든 메쉬의 material들의 컬러를 바꿔줍니다.

 

-  Boss 스크립트 기본 로직

/*Boss script */
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Boss : Enemy
{
    public GameObject missile;
    public Transform missilePortA;
    public Transform missilePortB;

    Vector3 lookVec; // 플레이어 움직임 예측
    Vector3 tauntVec; // 찍어내리는 벡터
    public bool isLook; // 플레이어 바라보는 플래그

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

        nav.isStopped = true;
        StartCoroutine(Think());
    }

    
    void Update()
    {
        if (isDead){
            StopAllCoroutines();
            return;
        }

        if (isLook)
        {
            float h = Input.GetAxisRaw("Horizontal");
            float v = Input.GetAxisRaw("Vertical");
            lookVec = new Vector3(h, 0, v) * 5f;
            transform.LookAt(Target.position + lookVec);
        }
        else
            nav.SetDestination(tauntVec);
    }
     
    IEnumerator Think(){}

    IEnumerator MissileShot() {}

    IEnumerator RockShot(){}

    IEnumerator Taunt() {}


}
  • 상속시 Awake() 함수는 자식 스크립트 단독 실행이므로, 보스 스크립트에서 Enemy의 스크립트에서 public으로 만들어 둔 rigid, boxCollider, meshs, nav, anim에 초기화 시켜줍시다.
  • Update() 구문에선, Boss가 바라보는 방향을 플레이어가 가려는 값을 넣은 lookVec를 만들어주었습니다.
    • 이로인해 플레이어가 공격을 피하기 쉽지 않게 될 예정입니다.
  • 죽을 시 모든 공격 코루틴은 멈춥니다.

 

 

🧷 4. 보스 공격 패턴 로직

-  공격 패턴 무작위 기능

/*Boss script */
public class Boss : Enemy
{

    IEnumerator Think()
    {
        yield return new WaitForSeconds(0.1f);

        int ranAction = Random.Range(0, 5); // 0 ~ 4

        switch (ranAction)
        {
            case 0:
            case 1:
                // 미사일 발사 패턴
                StartCoroutine(MissileShot());
                break;
            case 2:
            case 3:
                // 돌 굴러가는 패턴
                StartCoroutine(RockShot());
                break;
            case 4:
                // 점프 공격 패턴
                StartCoroutine(Taunt());
                break;

        }
    }

    IEnumerator MissileShot() {}

    IEnumerator RockShot(){}

    IEnumerator Taunt() {}


}
  • Random 함수를 사용하여, 확률별로 어떤 공격 패턴을 사용할지 정해줍니다.

 

-  미사일 공격 패턴

/*Boss script */
public class Boss : Enemy
{

    IEnumerator MissileShot()
   {
        anim.SetTrigger("doShot");
        yield return new WaitForSeconds(0.2f);
        GameObject instantMissileA = Instantiate(missile, missilePortA.position, missilePortA.rotation);
        BossMissile bossMissileA = instantMissileA.GetComponent<BossMissile>();
        bossMissileA.target = Target;

        yield return new WaitForSeconds(0.3f);
        GameObject instantMissileB = Instantiate(missile, missilePortB.position, missilePortB.rotation);
        BossMissile bossMissileB = instantMissileB.GetComponent<BossMissile>();
        bossMissileB.target = Target;

        yield return new WaitForSeconds(2.5f);

        StartCoroutine(Think());
    }
    
    
    IEnumerator RockShot(){}

    IEnumerator Taunt() {}


}
  • 애니메이션 트리거를 통하여, 애니메이션 동작을 하게 해줍니다
  • 미리 만든 Boss Missile을 인스턴트화하여 타겟만 정해줘, 미사일이 플레이어를 따라오게 만들어줍니다.
  • 이후 다음 공격 패턴을 위해 Think() 코루틴을 호출해 줍니다.

 

-  돌 공격 패턴

/*Boss script */
public class Boss : Enemy
{
    
    IEnumerator RockShot()
    {
        isLook = false;
        anim.SetTrigger("doBigShot");

        Instantiate(bullet, transform.position, transform.rotation);
        yield return new WaitForSeconds(3f);

        isLook = true;
        StartCoroutine(Think());
    }

    IEnumerator Taunt() {}


}
  • 돌 공격은 보스가 회전 움직임을 멈춰서 쏘기에 isLook 트리거를 false로 두었습니다
  • 이후 애니메이션 트리거를 동작
  • 돌은 인스턴트화 해주기만 하여도, BossRock 자체에서 굴러가는게 구현이 되어었습니다.

 

-  점프 공격 패턴

/*Boss script */
public class Boss : Enemy
{
    IEnumerator Taunt() 
    {
        tauntVec = Target.position + lookVec;
        isLook = false;
        nav.isStopped = false;
        boxCollider.enabled = false;
        anim.SetTrigger("doTaunt");

        yield return new WaitForSeconds(1.5f);
        melleArea.enabled = true;

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

        yield return new WaitForSeconds(1f);
        isLook = true;
        nav.isStopped = true;
        boxCollider.enabled = true;

        StartCoroutine(Think());
    }
}
  • tauntVec = 플레이어 위치 + lookVec(플레이어 움직임 예측) 을 넣어줍시다
  • 꺼두었던 navigate는 nav.isStopped =false로 다시 켜주고, 처음엔 박스콜라이더를 비활성화시킵니다.
  • 이후, 코루틴을 통해 1.5초 이후에 콜라이더를 활성화시키고, 0.5초 이후에는 꺼줍니다.
  • 이후 isLook, nav.isStopped, boxCollider까지 true로 제 위치를 시켜줍니다.

 

🧷 5. 마지막 오브젝트 점검 및 플레이어 넉백

-  오브젝트 점검

  • Boss 스크립트에는 각각에 맞게 오브젝트들이 들어가있어야합니다.
  • Target = Player
  • Melle Area = Boss Melee Area
  • Bullet = Boss Rock
  • Missile = Missile Boss
  • Missile Port A,B = Missile Port A,B
  • isLook 체크 (바라보는게 디폴트값으로 되어있어야합니다.)

 

  • 각 공격에 대한 오브젝트 Tag, Layer을 EnemyBullet으로 맞춰야합니다
  • 또 Bullet 스크립트는 각각에 맞게 체크해주고, 데미지를 넣어줍시다.

-  플레이어 넉백기능

/* Player script , 추가되는 부분만 작성하였습니다. */

    private void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Item") {}
        else if (other.tag == "EnemyBullet") {
            if (!isDamage) {
                bool isBossAtk = other.name == "Boss Melee Area";
                StartCoroutine(OnDamage(isBossAtk));
            }

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


    }

    IEnumerator OnDamage(bool isBossAtk) 
    {

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

        yield return new WaitForSeconds(1f);
        
        if (isBossAtk)
            rigid.velocity = Vector3.zero;


    }
  • 플레이어 OnDamage 함수에 매개변수로 isBossAtk라는 bool 값을 넘겨주어 플레이어에게 넉백기능을 줍니다.

 

-  소소한 고치기, 미사일 한개 맞을 시 , 다른 맞는 공격들은 다 제거해줍시다.

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

 

 

 

후후~  이번 강의에서는 보스를 만들면서, 보스의 공격 패턴 몇가지를 배우게 됐습니다.

이를 이용해서 플레이어의 스킬도 충분히 만들어 볼 수 있겠다고 느끼고, 상속에 대해서 더 이해하기 쉽게 사용했습니다.

 

 

출처: 골든메탈님 유튜브

https://www.youtube.com/watch?v=7JlujO3JYas&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy&index=13 

 

Contents

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

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