개발/개발일지

Tales Saga Chronicle Blast 개발일지 19 - 부활, 적 공격, 점수, 거리 연장 함수 등 구현

메피카타츠 2023. 3. 23. 06:33

 

public IEnumerator HitByEnemy(GameObject midoriPlane, GameObject enemy)
    {
        if (!_isAlivePlane) yield break;

        _isCanMove = false;
        _isAlivePlane = false;
        

        midoriPlane.GetComponent<Image>().color = Color.clear;
        StartCoroutine(ShowExplosion(midoriPlane));
        audioManager.PlaySFX("EnemyDestroy");

        if (enemy.CompareTag("EnemyBullet"))
        {
            enemy.SetActive(false);
        }

        StartCoroutine(MidoriPlaneRevive());
    }

 

먼저 HitByEnemyBullet과 HitByEnemy 함수를 통합했다. 다행히도 적 총알은 바로 없애도 문제가 생기지 않아 총알이면 바로 사라지도록 했다.

private IEnumerator ShootingGameTimer()
    {
        while(true)
        {
            if (_skill1Cooltime > 0)
            {
                skill1CooltimeText.text = _skill1Cooltime.ToString("F1");
                _skill1Cooltime -= 0.1f;
            }
            else if (_skill1Cooltime <= 0 && !_isReadySkill1)
            {
                _isReadySkill1 = true;
                skill1CooltimeText.text = "";
                skill1Background.GetComponent<Image>().color = Color.clear;
            }

            if (_skill2Cooltime > 0)
            {
                skill2CooltimeText.text = _skill2Cooltime.ToString("F1");
                _skill2Cooltime -= 0.1f;
            }
            else if (_skill2Cooltime <= 0 && !_isReadySkill2)
            {
                _isReadySkill2 = true;
                skill2CooltimeText.text = "";
                skill2Background.GetComponent<Image>().color = Color.clear;
            }

            yield return new WaitForSeconds(0.1f);
        }
    }

효율적인 작동을 위해 bool 타입의 변수 _isReadySkill1, _isReadySkill2를 추가해서 Timer에서 공격, 스킬이 준비될 때마다 한 번씩만 동작하도록 제한을 걸어줬다.

// Z키를 눌러 총알을 발사
        if (Input.GetKeyDown(KeyCode.Z))
        {
            // 공격은 기본적으로 연타하기 때문에 경고 표시하지 않도록 함
            if (_skill1Cooltime <= 0)
            {
                _isReadySkill1 = false;
                skill1Background.GetComponent<Image>().color = new Color(0, 0, 0, 0.8f);
                _skill1Cooltime = s_skill1CoolTime;
                StartCoroutine(ShootMidoriBullet(new Vector2(midoriPlane.GetComponent<RectTransform>().anchoredPosition.x, 570)));
            }
        }

총알에도 재사용 대기시간을 걸어줬다. 이런 변수들은 쉬운 변경을 위해서 상수로 선언해주었다. 0.2초는 너무 공격이 빠르고, 0.3초 정도가 적당해서 0.3초로 설정했다. 공격은 보통 연타로 누르기 때문에 누를 때마다 경고가 뜨면 정신없을 것 같아 경고를 따로 띄우지 않도록 하였다.

private IEnumerator MidoriPlaneRevive()
    {
        yield return new WaitForSeconds(1);
        if(_life <= 0)
        {
            // 게임 오버
            yield break;
        }
        else
        {
            --_life;

            if (_life == 1) lifeParent.transform.GetChild(1).gameObject.SetActive(false);
            else if (_life == 0) lifeParent.transform.GetChild(0).gameObject.SetActive(false);
        }

        midoriPlane.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, -600);
        midoriPlane.GetComponent<Image>().color = Color.white;
        midoriPlane.GetComponent<RectTransform>().DOLocalMoveY(-500, 2);

        yield return new WaitForSeconds(2);

        _isCanMove = true;

        // 1초간 무적 적용
        yield return new WaitForSeconds(1);

        _isAlivePlane = true;
    }

부활 기능도 추가하였다. 목숨이 다 닳으면 게임 오버 함수를 호출하도록 했다.

부활 후, 화면 하단 중앙에서 화면으로 2초에 걸쳐 등장하도록 했다.

적이 부활 장소에 있으면 불합리하게 사망할 수 있기 때문에 등장한 후, 1초간 무적을 적용했다.

 

그러다보니 기존에는 OnTriggetEnter2D를 사용했는데, 무적 시간 때 적과 겹치면 무적이 풀려도 죽지 않는 문제가 있어 OnTriggerStay2D로 교체했다.

 

// 적의 총알 객체를 발사하는 함수
    public IEnumerator ShootEnemyBulletCoroutine(GameObject enemy)
    {
        GameObject bullet = GetPrefab("EnemyBullet");

        bullet.SetActive(true);
        bullet.GetComponent<Image>().color = Color.white;

        bullet.GetComponent<RectTransform>().anchoredPosition = new Vector2(enemy.GetComponent<RectTransform>().anchoredPosition.x,
            enemy.GetComponent<RectTransform>().anchoredPosition.y - 50);

        Vector2 direction = ExtendBulletDirection(bullet.GetComponent<RectTransform>().anchoredPosition, midoriPlane.GetComponent<RectTransform>().anchoredPosition);
        LookRotation2D(bullet, direction);

        float bulletDuration = Vector2.Distance(bullet.GetComponent<RectTransform>().anchoredPosition, direction) / 600 * s_enemyBulletSpeed;
        bullet.GetComponent<RectTransform>().DOLocalMove(direction, bulletDuration).SetEase(Ease.Linear);

        yield return new WaitForSeconds(bulletDuration);
        bullet.GetComponent<Image>().color = Color.clear;
        bullet.SetActive(false);
        _enemyBulletQueue.Enqueue(bullet);
    }

적이 총알을 발사하는 기능도 추가했다.

아군이 총알을 발사하는 기능과 유사했지만 약간 차이가 있어 별도의 함수를 두었다.

private IEnumerator EnemyAttack()
    {
        while(true)
        {
            if (!ShootingGameManager.s_instance.GetIsAlivePlane())
            {
                _isReadyToAttack = false;
                yield return new WaitForSeconds(1);
            }
            else if (_isReadyToAttack == true)
            {
                ShootingGameManager.s_instance.ShootEnemyBullet(gameObject);
                yield return new WaitForSeconds(_attackDelay);
            }
            // 적 스폰 직후, 아군 부활 직후에 총알을 발사하지 않도록 최소 대기 시간 설정
            else
            {
                _isReadyToAttack = true;
                yield return new WaitForSeconds(1);
            }
        }
    }

위 함수를 적 개체마다 달려있는 EnemyController.cs에서 호출하는데, 아군이 죽었을 때는 공격을 하지 않고, 부활한 직후에도 최소 1초간 공격을 하지 않도록 했다.

여기서도 코루틴을 실행하면 재호출이 불가능해질테니 ShootingHameManager 스크립트 내에서 호출할 수 있도록 ShootEnemyBullet에서 ShootEnemyBulletCoroutine을 호출하도록 했다.

private void AddScore(int score)
    {
        _score += score;
        scoreText.text = _score.ToString();

        if(_score > s_highScore) highScoreText.text = _score.ToString();
    }

점수 기능도 구현했다. 적을 맞추거나 적을 처치하면 점수가 오르고, 최고 점수를 갱신하면 최고 점수 또한 갱신하도록 했다. 최고 점수를 갱신하면 노란색 글자로 알림을 띄워주는 것도 좋을 것 같다.

    // 총알의 목적지를 화면 밖까지 연장
    private Vector2 ExtendBulletDirection(Vector2 bulletPosition, Vector2 targetPosition)
    {
        Vector2 direction = targetPosition - bulletPosition;

        // 0으로 나누지 않도록 예외 처리
        if(direction.x == 0)
        {
            direction.x = bulletPosition.x;
            direction.y = (direction.y < 0) ? s_yDownEnd : s_yUpEnd;

            return direction;
        }
        else if(direction.y == 0)
        {
            direction.x = (direction.x < 0) ? s_xLeftEnd : s_xRIghtEnd;
            direction.y = bulletPosition.y;

            return direction;
        }

        float xToEnd, yToEnd, xToEndRate, yToEndRate;

        // x, y축 기준 화면 밖으로 이동하기 위해 필요한 거리
        xToEnd = (direction.x < 0) ? s_xLeftEnd - bulletPosition.x : s_xRIghtEnd - bulletPosition.x;
        yToEnd = (direction.y < 0) ? s_yDownEnd - bulletPosition.y : s_yUpEnd - bulletPosition.y;
        
        // 화면 밖까지 나가려면 얼마나 이동해야 하는지 비율
        xToEndRate = xToEnd / direction.x;
        yToEndRate = yToEnd / direction.y;

        // 비율에 따라 거리 연장
        if (xToEndRate > yToEndRate)
        {
            direction.x *= yToEndRate;
            direction.x += bulletPosition.x;

            direction.y = (direction.y < 0) ? s_yDownEnd : s_yUpEnd;
        }
        else
        {
            direction.x = (direction.x < 0) ? s_xLeftEnd : s_xRIghtEnd;

            direction.y *= xToEndRate;
            direction.y += bulletPosition.y;
        }

        return direction;
    }

여기에 추가로 총알의 이동 거리를 연장하는 함수도 추가했다. 기존에는 아군이 스킬을 쓰면 적 위치까지만 이동하고, 적이 공격을 하면 아군이 위치한 곳까지만 총알이 이동했는데, 그 거리를 화면 밖까지 이동하도록 연장하는 함수이다.

단순히 거리를 연장해도 되긴 했는데, y축은 상관없었으나 x축 기준 화면 밖으로 나가버리면 UI에 표시되는 문제가 있었다. UI 뒤에 검은색 배경을 깔아서 가릴 수도 있지만, 왼쪽은 그렇게 해버리면 캐릭터가 가려져서 화면 밖까지 나가는 거리만큼만 연장했다. 조금 번거롭긴 하지만 이러면 총알도 금방 회수되어 효율도 올라갈 것 같다.

 

 

슈팅 게임에서 남은 목표는 아래와 같다.

 

1. 스테이지 설계하기

2. 최고 점수 갱신시 효과 추가

3. 적 종류별 밸런스 구현

4. 적에게 공격당했을 때 미도리 애니메이션 추가

5.. 보스 스킬 / 패턴 / 체력바 등 구현

 

 

드디어 슈팅 게임의 전반적인 기능 구현이 끝났다! 몇몇 간단한 기능을 제외하면 스테이지 설계만 하면 될 것 같다.

개발하다보면 추가되는 기능도 몇몇 있을 것 같은데, 많진 않을 것 같다. 개발 착수 이후 딱 1주일 정도 걸린 것 같다. 개발에 걸린 실제 시간을 생각해보면 부지런히 개발했으면 3일 정도면 완료했을 것 같다. 개발을 진행하면서 기획하다보니 목표가 명확하지 않았고, 이에 따라 개발 기간이 늘어난 것 같다. 앞으로는 게임 개발에 필요한 기능들을 우선적으로 기획하여 목표를 명확히 정하고, 소요 시간을 예상하여 목표를 세우는 것이 필요할 것 같다.