본문 바로가기

프로젝트/BossRush

[BossRush] 2-3일차 : 플레이어 공격 구현

728x90
반응형

어제와 오늘은 플레이어 공격을 구현했다!

벌써부터 이것저것 생각할게 많당


기획

굉장히 간단하게 기획하였고 이때만 해도 금방 될 줄 알았다....

앞으로는 조금이라도 디테일하게 기획하고 개발을 시작해야할거 같다...🫠


개발

여튼 2-3일차는 플레이어 공격을 구현했다!

 

무기는 검, 활, 지팡이 세 종류로 구분하였다.

그리고 활과 지팡이에서 나오는 투사체는 두 종류로 구분하였다.

나중에 아이템과 스탯 같은 기능까지 고려하여 구현한다고 했는데 과연 수정없이 가능할지...

하지만 펠리컨적 사고로 '일단 시도함'🐦

 

우선 무기와 투사체는 확장성을 고려하여 인터페이스로 만들었다.

// Iweapon
public interface IWeapon
{
    public void Attack();
}

// IProjectile
public interface IProjectile
{
    public void Firing();
}

 

IWeapon을 상속받는 세 종류 무기들

public class Sword : MonoBehaviour, IWeapon
{
    [SerializeField] private Transform pivot;
    [SerializeField] private float rotationSpeed;
    [SerializeField] private float angle;

    private float angleValue;
    private bool isAttack;

    public void Attack()
    {
        if (!isAttack)
        {
            StartCoroutine(WieldSword());
        }
    }

    private void OnDisable()
    {
        StopCoroutine(WieldSword());
        pivot.rotation = Quaternion.Euler(Vector3.zero);
        isAttack = false;
    }

    private IEnumerator WieldSword()
    {
        isAttack = true;

        float targetAngle = angle;
        int phase = 1;

        while (phase <= 3)
        {
            float currentAngle = NormalizeAngle(pivot.rotation.eulerAngles.z);

            pivot.rotation = Quaternion.RotateTowards(pivot.rotation, Quaternion.Euler(0, 0, targetAngle), rotationSpeed);

            if (Mathf.RoundToInt(currentAngle) == Mathf.RoundToInt(targetAngle))
            {
                phase++;
                switch (phase)
                {
                    case 2:
                        targetAngle = -angle;
                        break;
                    case 3:
                        targetAngle = 0;
                        break;
                }
            }

            yield return new WaitForSeconds(0.005f);
        }

        isAttack = false;
    }

    private float NormalizeAngle(float angle)
    {
        if (angle > 180f)
        {
            angle -= 360f;
        }
            
        return angle;
    }
}
public class Bow : MonoBehaviour, IWeapon
{
    [SerializeField] private GameObject arrowPrefab;

    private GameObject arrowObject;
    private Arrow arrow;

    public void Attack()
    {
        if(arrowObject == null)
        {
            arrowObject = Instantiate(arrowPrefab);
        }
        
        arrowObject.transform.position = transform.position;
        arrow = arrowObject.GetComponent<Arrow>();

        arrow.Firing();
    }
}
public class Staff : MonoBehaviour, IWeapon
{
    [SerializeField] private Transform pivot;
    [SerializeField] private float rotationSpeed;
    [SerializeField] private float angle;
    [SerializeField] private SpriteRenderer crystal;
    [SerializeField] private Color glowColor;
    [SerializeField] private GameObject fireballPrefab;

    private float angleValue;
    private bool isAttack;
    private GameObject fireballObject;
    private Fireball fireball;

    public void Attack()
    {
        if (fireballObject == null)
        {
            fireballObject = Instantiate(fireballPrefab);
        }

        fireballObject.transform.position = transform.position;
        fireball = fireballObject.GetComponent<Fireball>();

        if (!isAttack)
        {
            StartCoroutine(WieldStaff());
            fireball.Firing();
        }
    }

    private void OnDisable()
    {
        StopCoroutine(WieldStaff());
        pivot.rotation = Quaternion.Euler(Vector3.zero);
        isAttack = false;
    }

    private IEnumerator WieldStaff()
    {
        isAttack = true;
        Color originalColor = crystal.color;
        float targetAngle = -angle;
        int phase = 1;
        crystal.color = glowColor;

        while (phase <= 2)
        {
            float currentAngle = NormalizeAngle(pivot.rotation.eulerAngles.z);

            pivot.rotation = Quaternion.RotateTowards(pivot.rotation, Quaternion.Euler(0, 0, targetAngle), rotationSpeed);

            if (Mathf.RoundToInt(currentAngle) == Mathf.RoundToInt(targetAngle))
            {
                phase++;

                if(phase == 2)
                {
                    targetAngle = 0;
                }
            }

            yield return new WaitForSeconds(0.005f);
        }

        isAttack = false;
        crystal.color = originalColor;
    }

    private float NormalizeAngle(float angle)
    {
        if (angle > 180f)
        {
            angle -= 360f;
        }

        return angle;
    }
}

 

검과 지팡이의 휘두르는 모션은 코루틴에서 Quaternion.RotateTowards 함수로 rotation 값을 수정해주었다.

오일러 각도는 음수가 적용되지 않아 정규화해주는 간단 함수를 따로 만들었다.

공격시 검은 위, 아래로 크게 휘두르고 제자리로 돌아오고 지팡이는 아래로 살짝 휘두르고 제자리로 돌아온다.

활은 투사체인 화살만 발사해준다.

전체적으로 중복 공격을 막기 위해 isAttack bool 변수를 만들어 코루틴 시작과 끝에서 변경 해주었다.

또한 무기 변경이 가능하기 때문에 OnDisable에 설정을 초기화 해주었다.

(앗 근데 지금 보니 지팡이 수정 색은 초기화 안해줬넹😅)

 

IProjectile를 상속받는 두 종류 투사체들

public class Arrow : MonoBehaviour, IProjectile
{
    [SerializeField] private Vector2 point;
    [SerializeField] private float duration;

    private bool isFiring;

    public void Firing()
    {
        if (!isFiring)
        {
            gameObject.SetActive(true);
            StartCoroutine(FiringArrow());
        }
    }

    private void OnDisable()
    {
        StopCoroutine(FiringArrow());
        isFiring = false;
    }

    private IEnumerator FiringArrow()
    {
        isFiring = true;

        Vector3 p0 = transform.position;
        Vector3 p1 = transform.position + new Vector3(point.x / 2, point.y, 0);
        Vector3 p2 = transform.position + new Vector3(point.x, 0, 0);

        float timeElapsed = 0f;

        while (timeElapsed < duration)
        {
            float t = timeElapsed / duration;

            Vector3 position = CalculateBezierPoint(t, p0, p1, p2);

            transform.position = position;

            timeElapsed += Time.deltaTime;

            yield return null;
        }

        gameObject.SetActive(false);
        isFiring = false;
    }

    private Vector3 CalculateBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2)
    {
        Vector3 p0p1 = Vector3.Lerp(p0, p1, t);
        Vector3 p1p2 = Vector3.Lerp(p1, p2, t);

        return Vector3.Lerp(p0p1, p1p2, t);
    }
}
public class Fireball : MonoBehaviour, IProjectile
{
    [SerializeField] private float point;
    [SerializeField] private float duration;

    private bool isFiring;

    public void Firing()
    {
        if (!isFiring)
        {
            gameObject.SetActive(true);
            StartCoroutine(FiringFireball());
        }
    }

    private void OnDisable()
    {
        StopCoroutine(FiringFireball());
        isFiring = false;
    }

    private IEnumerator FiringFireball()
    {
        isFiring = true;
        Vector3 startPoint = transform.position;
        Vector3 endPoint = transform.position + new Vector3(point, 0, 0);

        float timeElapsed = 0f;

        while (timeElapsed < duration)
        {
            float t = timeElapsed / duration;

            transform.position = Vector3.Lerp(startPoint, endPoint, t);

            timeElapsed += Time.deltaTime;

            yield return null;
        }

        gameObject.SetActive(false);
        isFiring = false;
    }
}

 

화살은 2차 베지어 곡선, 파이어볼은 직선으로 궤적을 구현하였다.

수식 구현은 Lerp함수를 활용하였다. (🐶🍯)

여기도 중복 발사를 막기 위해 bool 변수 isFiring을 사용하였다!

 

PlayerController에 추가된 무기 스왑 코드

private IWeapon currentWeapon;
private List<IWeapon> weaponComponents = new();
private List<GameObject> weaponObjects = new();

private void Start()
{
    var sword = GetComponentInChildren<Sword>(true);
    var bow = GetComponentInChildren<Bow>(true);
    var staff = GetComponentInChildren<Staff>(true);

    weaponComponents.Add(sword);
    weaponComponents.Add(bow);
    weaponComponents.Add(staff);

    weaponObjects.Add(sword.gameObject);
    weaponObjects.Add(bow.gameObject);
    weaponObjects.Add(staff.gameObject);

    currentWeapon = sword;
}

void Update() 
{
    if (Input.GetKeyDown(KeyCode.A))
    {
        SwapWeapon(0);
    } 
    else if(Input.GetKeyDown(KeyCode.S))
    {
        SwapWeapon(1);
    }
    else if (Input.GetKeyDown(KeyCode.D))
    {
        SwapWeapon(2);
    }

    if (Input.GetKeyDown(KeyCode.Z))
    {
        currentWeapon.Attack();
    }
}

private void SwapWeapon(int index)
{
    currentWeapon = weaponComponents[index];

    for(int i=0; i<weaponObjects.Count; i++)
    {
        weaponObjects[i].SetActive(i == index);
    }
}

Input.GetKeyDown은 switch문으로 바꾸는게 나을려나...

Start()부분 코드가 지저분한거 같아 마음에 안든다.

다음에 더 좋은 방법을 생각해서 리팩토링 해야징

 

그래두 아직까지는 하찮지만 나름 손맛(?)있는 조작감을 만든거 같다.

다음은 얼른 배경 대충 만들고 타격감을 위한 보스를 만들어야지!

728x90
반응형