Posted in: Unity, 프로젝트 When the Spring comes

21. 기본 원거리 몬스터 추가

기본 원거리 몬스터를 만들었다. 노란색 원 범위가 공격 범위이며 캐릭터가 범위안으로 들어왔을 시 공격을 하거나 거리를 벌린다.

막무가내로 원거리 몬스터를 만드는 게 아니라 구상도를 먼저 만들었다. 근거리 몬스터를 만들때도 구상도를 대충 그리긴 했지만 간략하게만 그려서 구상도라고 하기에도 뭐했다.

구상도가 있으니까 확실히 코드를 작성하다가 막히는 게 적었던 것 같다.

Idle 상태


public void Idle()
{
    if(!stateChange) StartCoroutine(StateChange());//상태변환

    inputX = 0;
    inputZ = 0;

    velocity = new Vector3(inputX, 0, inputZ);

    isMoving = false;
}

Idle 상태는 가만히 서있는 평상시 상태이다. 몬스터는 멈춰 있어야 한다. 중요한 것은 계속 가만히 서있는 게 아니라 움직이기도 하고 다른 방향으로 멈춰 서있기도 해야 한다. 상태가 변환하는 것은 코루틴으로 제어하였다.

Moving 상태


public void Moving()
{
    if(!stateChange) StartCoroutine(StateChange());//상태변환

    velocity = new Vector3(inputX, 0, inputZ);

    //애니메이션용 bool 변수
    if(velocity != new Vector3(0, 0, 0))
        isMoving = true;
    else
        isMoving = false;

    transform.position += velocity * monsterStat.speed * Time.deltaTime;

    Vector3 temp = transform.position - (transform.position + velocity*3);
    float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

    Direction(direction);
}

Moving 상태는 몬스터가 움직이는 상태이다. 그냥 움직이는 상태이며 딱히 플레이어를 추적하거나 하진 않는다. Idle과 Moving은 서로 코루틴에 의해 수시로 바뀐다.

중요한 것은 3D 프로젝트에 2D 스프라이트를 소스로 사용하므로 움직이는 위치에 따라 이동 방향은 일일이 구해주어야 한다. 3D 오브젝트를 게임 소스로 사용하면 Vector3.forward를 앞방향으로 설정하면 되지만 이 게임은 그럴 수가 없기 때문.

몬스터의 이동 방향은 애니메이션 출력이나 많은 곳에 사용되니 반드시 원하는 값을 구해야한다.

Tracking 상태


private void OnTriggerStay(Collider other) {
    if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
    {
        target = other.gameObject;
        targetPos = target.GetComponent<Transform>();

        //추적 범위에 플레이어가 들어왔을 시
        //추적 상태에 돌입
        if(!run) state = State.Tracking;
    }
}

private void OnTriggerExit(Collider other) {
    if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
    {
        //추적 범위를 벗어났을 시
        //아이들 상태에 돌입
        if(!run) state = State.Idle;

        target = null;
    }
}

기본 원거리 몬스터는 자식 오브젝트로 TargetArea를 상속한다. TargetArea는 SphereCollider만 연결되있는데 추적 범위로 사용하기 위해서이다. 추적 범위는 콜라이더의 Radius가 추적 범위가 된다.

OnTriggerStay와 Exit를 사용하여 ‘추적 범위에 들어왔을 시’ 그리고 ‘추적 범위에서 벗어났을 시’를 구분하였다.

public void Tracking()
{
    // 추격상태를 두 가지 경우로 두자.
    // 1. 범위안에 플레이어가 있을 시 추격
    // 2. 범위안에 플레이어 없을 시 Idle 상태
    if(target)
    {
        inputX = Mathf.Clamp(target.transform.position.x - transform.position.x, -1.0f, 1.0f);
        inputZ = Mathf.Clamp(target.transform.position.z - transform.position.z, -1.0f, 1.0f);

        velocity = new Vector3(inputX, 0, inputZ);

        //애니메이션용 bool 변수
        if(velocity != new Vector3(0, 0, 0))
            isMoving = true;
        else
            isMoving = false;

        transform.position += velocity * monsterStat.speed * Time.deltaTime;

        Vector3 temp = transform.position - target.transform.position;
        float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

        Direction(direction);

        // 추적 상태일시
        // 공격 범위에 진입할 수 있다.
        crAttack.attackAreaChecking = true;
    }
    else
    {
        state = State.Idle;
    }
}

OnTriggerStay에 의해 몬스터는 Tracking 상태가 되는데 Moving 함수와 다른 것은 플레이어 방향으로 이동한다는 것이다.

그리고 8방향 이동이 아니라 훤씬 부드럽게 이동한다. 이동 애니메이션 출력을 위해 이동 방향에 따라 8방향 중 하나로 지정해 주는 게 중요하다.

공격 범위 진입


//공격 기능은 다른 스크립트에서 관리//

void Update()
{
    // Tracking 상태일 때
    // 공격 범위에 진입할 수 있다
    if(attackAreaChecking) AttackArea();
}

void AttackArea()
{
    targets = Physics.OverlapSphere(this.transform.position, attackArea, whatIsLayer);

    for(int i = 0; i < targets.Length; ++i)
    {
        attackAreaChecking = false;
        // 거리 벌림 or 공격
        // 판별
        stateChange = true;
    }
}

기본 원거리 몬스터의 경우 Tracking 상태일 때 플레이어가 공격 범위에 진입한다면 공격 범위 진입 상태가 되는데 이때 랜덤으로 플레이어와 거리를 벌리느냐 공격하느냐 상태를 결정하게 된다.

공격 범위는 OverlapSphere로 간단하게 구현하였다. 공격 범위에 진입하면 stateChange에 의해 코루틴이 실행되고 일정 시간이 지나면 공격 기능과 도망 기능중에 호출되도록 하였다.

공격 상태


void Attack()
{
    attacking = false;

    // Commando Range 공격 애니메이션
    // 출력

    // 플레이어 방향
    Vector3 temp = targets[0].transform.position - transform.position;
    attackAngle = temp.normalized;

    Debug.Log("attackAngle: " + attackAngle);

    GameObject currentBullet = Instantiate(bullet, transform.position, Quaternion.identity);

    // 총알 발사 후 targets 비우기
    targets = null;



    targets = Physics.OverlapSphere(this.transform.position, attackArea, whatIsLayer);
    if(targets.Length >= 1)
    {
        for(int i = 0; i < targets.Length; ++i)
        {
            attackAreaChecking = false;
            // 거리 벌림 or 공격
            // 판별
            stateChange = true;
        }
    }
    else
    {
        // 공격하고 다시 공격범위안에 없다면
        // 움직일 수 있게
        crMovement.enabled = true;
    }

}

공격은 플레이어 방향으로 총알 한 발을 복제한다. 총알에는 생성되자 마자 특정한 속도로 발사되는 스크립트를 달아놨다.

중요한 것은 총알을 쏘고 다시 행동을 할 수 있게 하는 것인데 공격 범위안에 플레이어가 있는지 없는지 체크하고 그에 맞는 행동을 하게 하면된다. 플레이어가 있다면 공격을 다시 하거나 거리를 벌리고 없다면 추적 행동으로 돌아간다.

거리 벌림


public void Run()
{
    if(checking >= runTime)//도망가고 시간이 runTime만큼 흐르면 추적상태로 전환
    {
        state = State.Tracking;
        // 도망 상태 반드시 false로 만들자
        crAttack.runAway = false;
    }

    Vector3 temp = transform.position - targetPos.position;
    float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

    Direction(direction);
    directionVec = -directionVec;

    if(directionVec == new Vector3(-1.14f, 0, 1.14f))//UpLeft
    {
        inputX = -1.0f;
        inputZ = 1.0f;
    }
    if(directionVec == new Vector3(0, 0, 2.0f))//Up
    {
        inputX = 0f;
        inputZ = 1.0f;
    }
    if(directionVec == new Vector3(1.14f, 0, 1.14f))//UpRight
    {
        inputX = 1.0f;
        inputZ = 1.0f;
    }
    if(directionVec == new Vector3(-2.0f, 0, 0))//Left
    {
        inputX = -1.0f;
        inputZ = 0f;
    }
    if(directionVec == new Vector3(2.0f, 0, 0))//Right
    {
        inputX = 1.0f;
        inputZ = 0f;
    }
    if(directionVec == new Vector3(-1.14f, 0, -1.14f))//DownLeft
    {
        inputX = -1.0f;
        inputZ = -1.0f;
    }
    if(directionVec == new Vector3(0, 0, -2.0f))//Down
    {
        inputX = 0f;
        inputZ = -1.0f;
    }
    if(directionVec == new Vector3(1.14f, 0, -1.14f))//DownRight
    {
        inputX = 1.0f;
        inputZ = -1.0f;
    }

    velocity = new Vector3(inputX, 0, inputZ);
    transform.position += velocity * monsterStat.speed * Time.deltaTime;
}

플레이어로부터 도망간다고도 볼 수 있는 거리 벌림 기능이다. 플레이어를 얄밉게 만들기 위한 기능이라고 볼 수 있다.

몬스터 기준으로 플레이어가 있는 방향의 반대 방향을 구한 다음 방향에 따라 이동 위치를 결정하였다. 도망가는 시간은 runTime이 지나면 추적 상태로 바뀌게 하였다.

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

public class CommandoRangeMovement : MonoBehaviour
{
    /*
     *
     * Commando Range용 스크립트임!!
     *
     */

    private MonsterStat monsterStat;
    public CommandoRangeAttack crAttack;

    private Vector3 velocity;
    private float inputX;
    private float inputZ;
    public Vector3 directionVec;


    /*** 플레이어 오브젝트를         ***/
    /*** 레퍼런스로 받아오기 위해    ***/
    /*** 필요한 변수들              ***/
    private GameObject target;
    [SerializeField]//임시로 보게 함
    private Transform targetPos;
    private SphereCollider sphereCollider;
    public float overlapRadius;//추적 범위
    /*********************************/

    /*** 밑에서 부터 상태 식별용 변수들 ***/
    enum State {Idle, Moving, Tracking, Run}

    /* 인스펙트에 보이는 변수들 */
    public float stateChangeTime;//상태 변환 시간 제어
    /**************************/

    private bool stateChange;//State 바꾸기용 bool변수
    public bool isMoving;//애니메이션용 변수
    private bool idle;
    private bool moving;
    private bool tracking;
    [SerializeField]//임시로 보게 함
    private bool run;
    [SerializeField]//임시로 보게 함
    public float checking;
    public float runTime;


    private State state
    {
        set
        {
            switch(value)
            {
                case State.Idle:

                    idle = true;
                    moving = false;
                    tracking = false;
                    run = false;

                    break;

                case State.Moving:

                    moving = true;
                    idle = false;
                    tracking = false;
                    run = false;

                    break;

                case State.Tracking:

                    tracking = true;
                    idle = false;
                    moving = false;
                    run = false;

                    break;

                case State.Run:

                    run = true;
                    idle = false;
                    moving = false;
                    tracking = false;

                    break;
            }
        }
    }

    private void Start() {
        monsterStat = GetComponent<MonsterStat>();
        crAttack = GetComponent<CommandoRangeAttack>();
        sphereCollider = transform.Find("/Commando Range/TargetArea").GetComponent<SphereCollider>();
        sphereCollider.radius = overlapRadius;

        state = State.Idle;
        stateChange = false;
    }

    private void Update() {
        if(crAttack.runAway)
        {
            state = State.Run;
        }
        

        checking += Time.deltaTime;
    }

    private void FixedUpdate() {
        if(idle)
        {
            Idle();
        }
        else if(moving)
        {
            Moving();
        }
        else if(tracking)
        {
            Tracking();
        }
        else if(run)
        {
            Run();
        }
    }

    public void Idle()
    {
        if(!stateChange) StartCoroutine(StateChange());//상태변환

        inputX = 0;
        inputZ = 0;

        velocity = new Vector3(inputX, 0, inputZ);

        isMoving = false;

        transform.position += velocity * monsterStat.speed * Time.deltaTime;

        Vector3 temp = transform.position - (transform.position + velocity*3);
        float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

        Direction(direction); //몬스터 방향
    }

    public void Moving()
    {
        if(!stateChange) StartCoroutine(StateChange());//상태변환

        velocity = new Vector3(inputX, 0, inputZ);

        if(velocity != new Vector3(0, 0, 0))
            isMoving = true;
        else
            isMoving = false;

        transform.position += velocity * monsterStat.speed * Time.deltaTime;

        Vector3 temp = transform.position - (transform.position + velocity*3);
        float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

        Direction(direction);
    }

    public void Tracking()
    {
        // 플레이어가 범위안에 있다면 정상적으로 불리지만
        // 몬스터가 도망갔다가 추격상태로 전환 시에
        // 플레이어가 범위안에 없다면 target이 없기때문에
        // NullReferenceException이 발동된다.

        // 추격상태를 두 가지 경우로 두자.
        // 1. 범위안에 플레이어가 있을 시 추격
        // 2. 범위안에 플레이어 없을 시 Idle 상태
        if(target)
        {
            inputX = Mathf.Clamp(target.transform.position.x - transform.position.x, -1.0f, 1.0f);
            inputZ = Mathf.Clamp(target.transform.position.z - transform.position.z, -1.0f, 1.0f);

            velocity = new Vector3(inputX, 0, inputZ);

            //애니메이션용 bool 변수
            if(velocity != new Vector3(0, 0, 0))
                isMoving = true;
            else
                isMoving = false;

            transform.position += velocity * monsterStat.speed * Time.deltaTime;

            Vector3 temp = transform.position - target.transform.position;
            float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

            Direction(direction);

            // 추적 상태일시
            // 공격 범위에 진입할 수 있다.
            crAttack.attackAreaChecking = true;
        }
        else
        {
            state = State.Idle;
        }
    }

    public void Run()
    {
        /*  1. 플레이어와 반대되는 방향으로 도주
            *  2. 일정 거리가 되면 멈춤
            *  3. 다시 Tracking
            */

        if(checking >= runTime)//도망가고 시간이 runTime만큼 흐르면 추적상태로 전환
        {
            state = State.Tracking;
            // 도망 상태 반드시 false로 만들자
            crAttack.runAway = false;
        }

        Vector3 temp = transform.position - targetPos.position;
        float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

        Direction(direction);
        directionVec = -directionVec;

        if(directionVec == new Vector3(-1.14f, 0, 1.14f))//UpLeft
        {
            inputX = -1.0f;
            inputZ = 1.0f;
        }
        if(directionVec == new Vector3(0, 0, 2.0f))//Up
        {
            inputX = 0f;
            inputZ = 1.0f;
        }
        if(directionVec == new Vector3(1.14f, 0, 1.14f))//UpRight
        {
            inputX = 1.0f;
            inputZ = 1.0f;
        }
        if(directionVec == new Vector3(-2.0f, 0, 0))//Left
        {
            inputX = -1.0f;
            inputZ = 0f;
        }
        if(directionVec == new Vector3(2.0f, 0, 0))//Right
        {
            inputX = 1.0f;
            inputZ = 0f;
        }
        if(directionVec == new Vector3(-1.14f, 0, -1.14f))//DownLeft
        {
            inputX = -1.0f;
            inputZ = -1.0f;
        }
        if(directionVec == new Vector3(0, 0, -2.0f))//Down
        {
            inputX = 0f;
            inputZ = -1.0f;
        }
        if(directionVec == new Vector3(1.14f, 0, -1.14f))//DownRight
        {
            inputX = 1.0f;
            inputZ = -1.0f;
        }

        velocity = new Vector3(inputX, 0, inputZ);
        transform.position += velocity * monsterStat.speed * Time.deltaTime;
    }

    ////////////////////
    // Idle, Moving, Tracking
    // 상태 변환용 코루틴
    ////////////////////
    IEnumerator StateChange()
    {
        stateChange = true;

        inputX = Random.Range(-1, 2);
        inputZ = Random.Range(-1, 2);

        yield return new WaitForSeconds(stateChangeTime);
        
        //State.Idle = 0, State.Moving = 1, State.Tracking = 2
        //0과 1까지만 대입
        state = (State)Random.Range(0, 2);
        stateChange = false;
    }

    public void Direction(float dir)
    {
        if(dir >= -67.5f && dir < -25.5f)//UpLeft
        {
            directionVec = new Vector3(-1.14f, 0, 1.14f);
        }
        if(dir >= -112.5f && dir < -67.5f)//Up
        {
            directionVec = new Vector3(0, 0, 2.0f);
        }
        if(dir >= -157.5f && dir < -112.5f)//UpRight
        {
            directionVec = new Vector3(1.14f, 0, 1.14f);
        }
        if(dir >= -22.5f && dir < 22.5f)//Left
        {
            directionVec = new Vector3(-2.0f, 0, 0);
        }
        if(dir == 0)//Idle
        {
            directionVec = new Vector3(0, 0, 0);
        }
        if(dir >= -180.0f && dir <= -157.5f)
        {
            directionVec = new Vector3(2.0f, 0, 0);
        }
        if(dir >= 157.5f && dir <= 180.0f)//Right
        {
            directionVec = new Vector3(2.0f, 0, 0);
        }
        if(dir >= 22.5f && dir <= 67.5f)//DownLeft
        {
            directionVec = new Vector3(-1.14f, 0, -1.14f);
        }
        if(dir >= 67.5f && dir < 112.5)//Down
        {
            directionVec = new Vector3(0, 0, -2.0f);
        }
        if(dir >= 112.5f && dir < 157.5f)//DownRight
        {
            directionVec = new Vector3(1.14f, 0, -1.14f);
        }
    }

    private void OnTriggerStay(Collider other) {
        if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
        {
            target = other.gameObject;
            targetPos = target.GetComponent<Transform>();

            //추적 범위에 플레이어가 들어왔을 시
            //추적 상태에 돌입
            if(!run) state = State.Tracking;
        }
    }

    private void OnTriggerExit(Collider other) {
        if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
        {
            //추적 범위를 벗어났을 시
            //아이들 상태에 돌입
            if(!run) state = State.Idle;

            target = null;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CommandoRangeAttack : MonoBehaviour
{
    //  플레이어와 충돌 관련 변수   //
    //private SphereCollider sphereCollider;
    public float attackArea;
    public Collider[] targets;
    public LayerMask whatIsLayer;          

    // 공격 기능에 필요한 변수 //
    private PlayerStat playerStat;
    public Vector3 attackAngle;
    public float bulletSpeed;
    public GameObject bullet;

    // 상태 변환에 필요한 변수 //
    private CommandoRangeMovement crMovement;
    public float runOrAttackTime;
    public bool attackAreaChecking;
    [SerializeField]
    public bool runAway;
    [SerializeField]
    private bool attacking;
    private bool stateChange;

    // 테스트 //
    //public int colliderNumber;

    private void Start() {
        //sphereCollider = transform.Find("/Commando Range/AttackArea").GetComponent<SphereCollider>();
        playerStat = transform.Find("/Scopeia").GetComponent<PlayerStat>();
        crMovement = GetComponent<CommandoRangeMovement>();

        //코드 상으로 프리팹crBullet을 받아오고 싶은데.. 어카지..?
        //bullet = transform.Find("/Assets/Prefabs/crBullet").GetComponent<Bullet>();
    }

    private void Update() {
        // NonAlloc을 사용하고 싶은데 잘 안되네
        // 나중에 방법을 찾아보자
        //int collidersNumber = Physics.OverlapSphereNonAlloc(this.transform.position, attackArea, targets);

        //공격 범위 진입 체크
        if(attackAreaChecking) AttackArea();
        else if(stateChange) StartCoroutine(StateChange());


        // 거리 벌림 상태일때
        // 다시 움직일 수 있게.
        if(runAway) crMovement.enabled = true;

        if(attacking) Attack();

    }

    void AttackArea()
    {
        targets = Physics.OverlapSphere(this.transform.position, attackArea, whatIsLayer);

        for(int i = 0; i < targets.Length; ++i)
        {
            attackAreaChecking = false;
            // 거리 벌림 or 공격
            // 판별
            stateChange = true;
        }
    }

    void Attack()
    {
        attacking = false;

        // Commando Range 공격 애니메이션
        // 출력

        // 플레이어 방향
        Vector3 temp = targets[0].transform.position - transform.position;
        attackAngle = temp.normalized;

        Debug.Log("attackAngle: " + attackAngle);

        GameObject currentBullet = Instantiate(bullet, transform.position, Quaternion.identity);

        // 총알 발사 후 targets 비우기
        targets = null;



        targets = Physics.OverlapSphere(this.transform.position, attackArea, whatIsLayer);
        if(targets.Length >= 1)
        {
            for(int i = 0; i < targets.Length; ++i)
            {
                attackAreaChecking = false;
                // 거리 벌림 or 공격
                // 판별
                stateChange = true;
            }
        }
        else
        {
            // 공격하고 다시 공격범위안에 없다면
            // 움직일 수 있게
            crMovement.enabled = true;
        }

    }

    IEnumerator StateChange()
    {
        stateChange = false;

        // 공격 범위 진입 시
        // Movement 종료
        crMovement.enabled = false;

        yield return new WaitForSeconds(runOrAttackTime);

        // 랜덤으로 거리 벌림 or 공격 선택 //
        int temp = Random.Range(0, 2);
        if(temp == 0)
        {
            runAway = true;
            crMovement.checking = 0;
        }
        else if(temp == 1) attacking = true;
    }

    private void OnDrawGizmos() {
        Gizmos.color = Color.yellow;

        Gizmos.DrawWireSphere(this.transform.position, attackArea);
    }

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

public class Bullet : MonoBehaviour
{
    private CommandoRangeAttack crAttack;
    private MonsterStat monsterStat;

    private void Start() {
        crAttack = transform.Find("/Commando Range").GetComponent<CommandoRangeAttack>();
        monsterStat = transform.Find("/Commando Range").GetComponent<MonsterStat>();
        gameObject.GetComponent<Rigidbody>().AddForce(crAttack.attackAngle * crAttack.bulletSpeed);

    }

    private void OnTriggerStay(Collider other) {
        if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
        {
            PlayerStat playerStat = other.gameObject.GetComponent<PlayerStat>();
            playerStat.health -= monsterStat.damage;
            Debug.Log(other.gameObject.name + "남은 체력: " + playerStat.health);
        }

        // 플레이어 무기 콜라이더
        // 몬스터 콜라이더
        // 를 제외하고 닿으면 스스로 없앰
        if(other.gameObject.layer != LayerMask.NameToLayer("Monster") &&
           other.gameObject.layer != LayerMask.NameToLayer("Weapon"))
        {
            Destroy(gameObject);
        }
    }
}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다