호우동의 개발일지

Today :

article thumbnail

구현 목표


기획도
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

원형으로 보스 위에 몇 개의 매직볼이 생긴다.


이 매직 볼은 두 가지 종류가 있는데
파란색과 빨간색이다.

 

 

파란색 -> 느리지만 플레이어를 추적
빨간색 -> 빠르지만 플레이어 방향으로 일직선 공격


이를 구현해 보자

 

 

 

 

구현 과정


1. Magic Ball 오브젝트 설정

매직 볼 이펙트
매직 볼 이펙트

2가지 MagicBall에 어울릴만한 오브젝트를 선별했다.

 

 

 

 

 

일단 크기를 조절하기 위해서 Scene에서 크기를 확인해 봤다.

줄어들어야할 파티클
줄어들어야할 파티클

너무 크다. 그래서 Scale을 줄이고자 한다.
그런데 Scale을 아무리 건드려도 줄어들지 않을 것이다.


왜냐하면 해당 오브젝트는 파티클 시스템이기 때문에
다른 설정을 해줘야 한다.

 

 

 

 

파티클을 조절할거임
파티클 hierarchy로 바꿔서 크키 조정이 가능해지도록 함

이펙트에 파티클 시스템 컴포넌트를 보면 안에 Scaling Mode가 있다.
여기서 Hierarchy로 바꿔주고 스케일을 다시 조정해 주면 이제 Scale로 크기 조정이 가능해진다.

 

 

 

 

 

매직볼의 크기가 작아졌다.
매직볼의 크기가 작아졌다.

 

 

 

 

크기를 절반으로 줄였는데, 이 정도면 시야를 그렇게 가리지 않고 딱 적절한 것 같다.
이 것 말고도 두 번째 오브젝트도 똑같이 설정해 주고 프리팹을 만들어줬다.

똑같이 만들어준 아이스볼 프리팹
아이스볼 프리팹도 똑같이 해줌

 

 

 

 


2. Magic Ball 원형 소환 및 배치

 

코싸인 싸인 공식
원형 배치를 위한 수학 공식

원형 배치를 위해서는 해당 방식을 이해해야 한다.
1을 원의 반지름 길이라고 생각해 보자.


그렇다면 우리는 각도만 알고 있다면 (cos, sin)식으로 구할 수 있다.

 

 

 

이를 코드로 구현하면 된다.

 

 

 

List<AObstacle> balls = new List<AObstacle>(); 
balls.Clear(); // 리스트 초기화
int offset = 0;
// MAX_BALL_NUM 개수만큼의 매직 볼 랜덤 소환
while (balls.Count < MAX_BALL_NUM)
{
    // 반구에 균등하게 배분해서 생성
    float angle = offset * Mathf.PI / (MAX_BALL_NUM - 1);
    int rand = Random.Range(0, MagicBalls.Length) + OFFSET_OBJECTPOOL;
    AObstacle ball = ObjectPool.Instance.GetObject(rand).GetComponent<AObstacle>();
    ball.enabled = false;
    // 반 구 모양으로 소
    ball.transform.position = transform.position + (new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0)) * RADIUS;
    offset++;
    balls.Add(ball);
    yield return new WaitForSeconds(0.3f);
}

MAX_BALL_NUM은 생성할 MagicBall의 개수이다.
공의 개수를 저장하기 위해 리스트를 사용하였다.



offset으로 균등하게 배분한 각도마다
하나씩 ball을 생성하도록 한다.


angle을 보면 180도를 공의 개수만큼 나눈다.


1을 빼주는 이유는 인덱스가 0부터 시작하기 때문이다.

 

그 후 공의 위치를 자신의 현재 위치에서
cos(angle) sin(angle)만큼 떨어진 좌표에 위치시켜
원의 모양으로 위치시키도록 한다.



이는 코루틴으로 구성했기 때문에
공의 최대개수가 되기 전까지 0.3초마다 반복했다.



여기서 rand는 빨간 ball과 파란 ball이
랜덤 하게 생성하게 하기 위해서 준 값이다.


 

 

 

3. MagicBall 특성 주기

이제 각각 Ball마다 다르게 구성할 것이다.

 


빨강 MagicBall - 빠르고 일직선으로 플레이어 방향으로 날아감
파랑 MagicBall - 느리지만 회피가 가능한 플레이어를 추적함

 

각 MagicBall은 하나의 스크립트를 컴포넌트로 가지게 한다.
빨간색은 Straight_Ball.cs라고 한다.

public abstract class AObstacle : MonoBehaviour
{
    public Transform point;
    
    protected virtual void UpdateTarget()
    {
        Collider[] cols = Physics.OverlapSphere(transform.position, 100, LayerMask.GetMask("Player"));
        for (int i = 0; i < cols.Length; i++)
        {
            PlayerControl newTarget = cols[i].GetComponent<PlayerControl>();
            if (newTarget != null)
            {
                point = newTarget.transform;
                return;
            }
        }
        point = null;
    }
}

 

 

 

 

Straight_Ball은 AObstacle을 상속받는다.

public class Straight_Ball : AObstacle
{
    private const float BALL_SPEED = 80F;
    private const int LIFE_TIME = 4;
    private Vector3 dir;
    private Rigidbody rigid;
    // Update is called once per frame

    void Start()
    {
        StartCoroutine(ReturnObstacle(LIFE_TIME, 5));
        InvokeRepeating("UpdateTarget", 0, 0.25f);
        rigid = GetComponent<Rigidbody>();
        Invoke("SetBall",0.3f);
    }

    private void SetBall()
    {
        if (point == null) return;
        dir = (point.position - transform.position).normalized;
        dir = new Vector3(dir.x, 0, dir.z);
        rigid.AddForce(dir * BALL_SPEED,ForceMode.Impulse);
    }
}

 

일단 생성되자마자 5초 뒤에
다시 오브젝트풀링이 반환되도록 설정한다.


그리고 UpdateTarget으로 Player을 찾는다.


0.3초 뒤에는 SetBall로 일직선으로 Ball이 날아가도록 한다.

 

SetBall을 보면 간단하다.



(플레이어 위치 - ball 위치)를 하고
이를 정규화하여 방향벡터를 찾은 뒤


addForce로 그 방향으로 BALL_SPEED 만큼의 힘으로 날린다.

 


여기서 ForceMode.Impulse는
순간적인 힘을 줄 때 자주 쓰는 모드이다.

 

이제 파란색 Ball을 Chasing_Ball.cs로 만들겠다.
 이것도 역시 AObstacle을 상속받는다.

 

 

 

public class Chasing_Ball : AObstacle
{
    private const float MAX_SPEED = 40F;
    private const float ATTACK_OFFSET = 5F;
    private const float DECREASE_PERCENT = 0.3F;
    private const float ROTATION_SPEED = 80F;
    private const int LIFE_TIME = 10;

    private Rigidbody rigid;
    void Start()
    {
        InvokeRepeating("UpdateTarget", 0, 0.5f);
        StartCoroutine(ReturnObstacle(LIFE_TIME, 6));
        rigid = GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (rigid.velocity.magnitude > MAX_SPEED) rigid.velocity = rigid.velocity * DECREASE_PERCENT;
        rigid.AddForce(transform.forward, ForceMode.VelocityChange);
        if (point == null) return;
        Vector3 relativeDir = (point.position - transform.position) + Vector3.up * ATTACK_OFFSET;
        Quaternion dir = Quaternion.LookRotation(relativeDir);
        Debug.Log(dir);
        transform.rotation = Quaternion.Slerp(transform.rotation, dir, Time.deltaTime * ROTATION_SPEED);
    }
}

MAX_SPEED -> Ball이 가질 수 있는 최대 속도
ATTACK_OFFSET -> 공격 포인트 오프셋 (기존의 오프셋 y값이 너무 낮아서 보정했음)
DECREASE_PERCENT -> 속도 감소 퍼센트
ROTATION_SPEED -> 회전 속도(해당 값이 추적 능력을 결정)
LIFE_TIME -> Ball 유지 시간

 

 

 

기본적으로는 UpdateTarget으로 받아온 player 위치로
회전하면서 계속해서 가속도를 더한다.

 


그러다가 MAX_SPEED가 넘으면 감속하고
다시 플레이어 쪽으로 가속하는 것을 반복한다.


회전할 때 곧바로 돌리는 것이 아닌 Slerp 사용하여
천천히 돌리는 것을 사용한다.

 

관련된 전체코드는 이러하다.

 

 

 

Boss_Bishop.cs

private IEnumerator TeleportAction(Vector3Int point)
    {
        bossState = BossState.attack;
        MeshCollider colli = mesh.GetComponent<MeshCollider>();

        anim.SetBool("Teleport", true);
        yield return new WaitForSeconds(TELEPORT_SPEED);
        colli.enabled = true;
        mesh.enabled = true;
        anim.SetBool("Teleport", false);
        transform.position = point + Vector3.up * 5;
        teleportEffect.Play();

        bossState = BossState.idle;
        Invoke("OnAction", actionDelay);
    }
    private void ShootMagicBall()
    {
        if (target == null) return;
        StartCoroutine(MagicBallAction());
    }
    private IEnumerator MagicBallAction()
    {
        bossState = BossState.attack;

        // magic Ball 개수를 세기 위해 list에 저장
        List<AObstacle> balls = new List<AObstacle>(); 
        int count = REPEAT;

        while (count-- > 0)
        {
            balls.Clear(); // 리스트 초기화
            int offset = 0;
            // MAX_BALL_NUM 개수만큼의 매직 볼 랜덤 소환
            while (balls.Count < MAX_BALL_NUM)
            {
                // 반구에 균등하게 배분해서 생성
                float angle = offset * Mathf.PI / (MAX_BALL_NUM - 1);
                int rand = Random.Range(0, MagicBalls.Length) + OFFSET_OBJECTPOOL;
                AObstacle ball = ObjectPool.Instance.GetObject(rand).GetComponent<AObstacle>();
                ball.enabled = false;
                // 반 구 모양으로 소
                ball.transform.position = transform.position + (new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0)) * RADIUS;
                offset++;
                balls.Add(ball);
                yield return new WaitForSeconds(0.3f);
            }
            yield return new WaitForSeconds(1f);
            // 각각의 매직볼 액션 활성
            foreach (AObstacle ball in balls)
            {

                ball.enabled = true;
                ball.transform.LookAt(target);
            }
            yield return new WaitForSeconds(5f);
        }
        yield return new WaitForSeconds(7f);
        bossState = BossState.idle;
        Invoke("OnAction", actionDelay);
    }

 

 

 

 

Straight_Ball.cs

public class Straight_Ball : AObstacle
{
    private const float BALL_SPEED = 80F;
    private const int LIFE_TIME = 4;
    private Vector3 dir;
    private Rigidbody rigid;

    void Start()
    {
        StartCoroutine(ReturnObstacle(LIFE_TIME, 5));
        InvokeRepeating("UpdateTarget", 0, 0.25f);
        rigid = GetComponent<Rigidbody>();
        Invoke("SetBall",0.3f);
    }

    private void SetBall()
    {
        if (point == null) return;
        dir = (point.position - transform.position).normalized;
        dir = new Vector3(dir.x, 0, dir.z);
        rigid.AddForce(dir * BALL_SPEED,ForceMode.Impulse);
    }
}

 

 

 

 

 

Chasing_Ball.cs

public class Chasing_Ball : AObstacle
{
    private const float MAX_SPEED = 40F;
    private const float ATTACK_OFFSET = 5F;
    private const float DECREASE_PERCENT = 0.3F;
    private const float ROTATION_SPEED = 80F;
    private const int LIFE_TIME = 10;

    private Rigidbody rigid;
    void Start()
    {
        InvokeRepeating("UpdateTarget", 0, 0.5f);
        StartCoroutine(ReturnObstacle(LIFE_TIME, 6));
        rigid = GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (rigid.velocity.magnitude > MAX_SPEED) rigid.velocity = rigid.velocity * DECREASE_PERCENT;
        rigid.AddForce(transform.forward, ForceMode.VelocityChange);
        if (point == null) return;
        Vector3 relativeDir = (point.position - transform.position) + Vector3.up * ATTACK_OFFSET;
        Quaternion dir = Quaternion.LookRotation(relativeDir);
        Debug.Log(dir);
        transform.rotation = Quaternion.Slerp(transform.rotation, dir, Time.deltaTime * ROTATION_SPEED);
    }
}

 

 

 

 

 

 

구현 결과


완성된 패턴
정상적으로 패턴이 나가는걸 확인할 수 있음

원하는 대로 의도된 패턴이 나오는 것을 확인할 수 있다.
나름 잘 구현된 것 같다. 설명을 좀 많이 개떡같이 한 것 같다.