ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 최적화 - 1
    Galaxy Ball/5. 최적화 2024. 9. 10. 21:30

    게임을 시스템적으로 완성한지는 꽤 많은 시간이 흘렀다.

    그럼에도 불구하고 게임을 최종적으로 출시하지 못한 이유는 바로 최적화!

     

    테스트 용으로 핸드폰에 빌드하여 게임을 실행해본 결과

    프레임이 플레이에 지장이 갈 정도로 뚝뚝 끊기는것을 확인할 수 있었다

     

    지금까지 단 한번도 배워본적이 없는 부분이었다. 물론 효율적으로 코드를 짜는법은 얼추 배웠으나

    이런 일이 생겼을때 어떻게 해결해야 하는지는 구제적으로 배워본적이 없다

     

    지금까지 수많은 가능성을 유추해보았지만 가장 가능성이 큰 건 바로 코드이다

     

    실제 사용된 코드 중 일부. 저 긴게 전부 Update 메서드안에 들어았다...

    사실상 Galaxy Ball은 게임 개발이라는것을 처음 배울때부터 만들어왔기 때문에 초반에 짠 코드들은 말할것도 없고

    가장 최근에 짠 코드들도 전부 엉망진창인 것을 확인할 수 있다 

     

    코드의 효율성, 최적화 이런것들은 전부 내팽겨쳐놓고 일단 원하는대로 구동만 되면 상관없다는 생각으로 만들어왔었다

    심지어 개발은 성능좋은 컴퓨터로 해왔기 때문에 이게 폰에서 구동될때의 문제점은 1도 생각하지 않고 있었다

     

    하지만 폰으로 확인해본 결과는 처참했고 결국 이런 코드들이 쌓이고 쌓여 문제점들이 생긴게 아닌가. 

    이걸 가장 큰 원인으로 생각하고 있다.

     

    전에 가르쳐주던 선생님께 조언을 구할지, 누군가에게 물어볼지 고민도 했었으나 책임감을 가지고

    직접 될때까지 코드를 뜯어고쳐 해결해보도록 하겠다

     

     

    • 사전 캐싱: 반복적으로 사용하는 객체들은 Start나 Awake 메서드에서 미리 캐싱해두는 것이 좋습니다. 이는 반복적으로 실행될 때마다 오버헤드를 줄여줍니다.
    • 객체 관리: 필요할 때마다 객체를 찾아서 사용하는 대신, 필요한 경우 게임 오브젝트들을 리스트로 관리하거나, 변수가 변경될 때만 검사하는 로직을 추가할 수 있습니다.
    • 태그 사용 최소화: 태그를 사용해 오브젝트를 찾는 대신, 게임 매니저가 필요한 오브젝트들을 직접 관리하게 하는 것이 좋습니다. GameObject.FindGameObjectsWithTag
    • Debug.Log는 개발 중에는 유용하지만, 특히 모바일 환경에서 많은 양의 로그 출력은 성능에 영향을 줄 수 있습니다.

    코드 최적화에 중요함과 동시에 내 코드들의 주 문제점들을 챗지피티를 통해 정리해보았다.

     

    우선 가장 먼저 해볼것은 첫번째,

    매 프레임마다 반복되는 update 메서드 안에 들어간 너무 많은 코드들을 빼내볼 생각이다

     

     private void Update()
        {
    
            if (Input.GetMouseButtonDown(0))
            {
                clickPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                clickPosition.z = 0f;
                Collider2D[] colliders = Physics2D.OverlapPointAll(clickPosition);
                foreach (Collider2D collider in colliders)
                {
                    if (collider.gameObject == P1firezone)
                    {
                        if (fireitem != null)
                        {
                            Instantiate(fireitem, clickPosition, Quaternion.identity);
                            Debug.Log("P1이 아이템을 사용하였습니다");
                            Debug.Log("아이템의 이름은 " + fireitem.gameObject.name + "입니다");
                            isDragging = true;
                            fireitem = null;
                            break;
                        }
                        else
                        {
                            Instantiate(P1ballPrefab, clickPosition, Quaternion.identity);
                            Debug.Log("P1이 기본구체를 날렸습니다");
                            isDragging = true;
                            break;
                        }
                    }
                }
            }
    
            if (isDragging && Input.GetMouseButtonUp(0))
            {
                Vector3 currentPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                currentPosition.z = 0f;
                GameManager.shotDistance = Vector3.Distance(clickPosition, currentPosition)*2;
                Vector3 dragDirection = (currentPosition - clickPosition).normalized;
                GameManager.shotDirection = dragDirection;
                isDragging = false;
            }
    
            int totalBalls = GameObject.FindGameObjectsWithTag("EnemyBall").Length +
                           GameObject.FindGameObjectsWithTag("P1ball").Length;
            if (totalBalls > 16)
            {
                SceneManager.LoadScene("Fail");
            }
    
            int totalenemy = GameObject.FindGameObjectsWithTag("Enemy").Length;
            if (totalenemy <= 0)
            {
                gameManager = FindObjectOfType<StageGameManager>();
                if (gameManager.StageClearID == StageState.chooseStage && gameManager.StageClearID != 5)
                {
                    gameManager.StageClearID += 1;
                    gameManager.SaveStageClearID();
                }
                if (gameManager.StageClearID == 5)
                {
                    gameManager.StageClearID += 0.5f;
                    gameManager.SaveStageClearID();
                }
                SceneManager.LoadScene("Clear");
            }
        }

     

     싱글플레이 인게임을 관리하는 중요한 코드 SPGameManager의 일부

    지금 위에 보이는 코드들이 전부 Update 메서드안에 우겨들어가 있다. 이러니 프레임이 끊길수밖에...

     

    자 이제 이걸 한 덩어리씩 Update 안에서 빼내보겠다ㅋㅋ

     

     int totalBalls = GameObject.FindGameObjectsWithTag("EnemyBall").Length +
                           GameObject.FindGameObjectsWithTag("P1ball").Length;
            if (totalBalls > 16)
            {
                SceneManager.LoadScene("Fail");
            }
    
            int totalenemy = GameObject.FindGameObjectsWithTag("Enemy").Length;
            if (totalenemy <= 0)
            {
                gameManager = FindObjectOfType<StageGameManager>();
                if (gameManager.StageClearID == StageState.chooseStage && gameManager.StageClearID != 5)
                {
                    gameManager.StageClearID += 1;
                    gameManager.SaveStageClearID();
                }
                if (gameManager.StageClearID == 5)
                {
                    gameManager.StageClearID += 0.5f;
                    gameManager.SaveStageClearID();
                }
                SceneManager.LoadScene("Clear");
            }
        }

     

    우선 이 부분. 위 코드는 인게임 내에서 승패를 결정하는 코드이다

     

    코드를 짤 당시에는 "그냥 실시간으로 계속 확인하다가 조건을 만족시키는 순간 씬변환을 해주면 되는거 아닌가?"

    라는 생각으로 Update안에 넣었었다. 사실상 여기 넣는게 제일 편하기도 했었고...

    하지만 이제 아니다. Update 메서드에 너무 많은 코드가 있을 경우 프레임 성능에 영향을 줄 수 있다

    이제 이걸 한번 빼보도록 하자

     

    이 코드는 크게 두가지 역할을 수행한다

     

    1. 공의 개수(totalBalls)가 16개가 넘어가면 "Fail"씬으로 변환

    2. 적의 수(GameObject.FindGameObjectsWithTag("Enemy").Length)가 0이되면 "Clear"씬으로 변환

     

    가장 먼저 공의 개수를 확인하는 1번부터 살펴보자.

    기존의 코드는 실시간으로 태그가 "EnemyBall" 인것과 "P1Ball"인것의 오브젝트 개수를 더해

    그걸 totalBall에 추가했다

     

    하지만 이건 좋은 방법이 아니었다. 

    • 태그 사용 최소화: 태그(GameObject.FindGameObjectsWithTag)를 사용해 오브젝트를 찾는 대신, 게임 매니저가 필요한 오브젝트들을 직접 관리하게 하는 것이 좋습니다. 

    위에 적어둔 3번째 내용. 태그를 사용하여 오브젝트를 찾는것은 좋은 방법이 아니라고 한다

    게다가 그 태그를 update내에서 프레임마다 찾고 있었으니...프레임이 안끊기는게 이상할 것이다

     

    private int totalBalls = 0;
    
    public void AddBall()
    {
        totalBalls++;
        CheckBallLimit();
    }
    
    public void RemoveBall()
    {
        totalBalls--;
        CheckBallLimit();
    }
    
    private void CheckBallLimit()
    {
        if (totalBalls > 16)
        {
            SceneManager.LoadScene("Fail");
        }
    }

     

    그래서 사용한 방법은 이런 느낌이다

    이제 실시간으로 태그를 이용하여 개수를 찾는게 아니라 공이 생성될때와 파괴될때만 메서드를 실행하여

    totalBalls를 관리해주는것이다

     

    우선 전에 있던 코드를 지우고 update 뒤에 새로운 메서드 3개를 추가해주었다

     

    그럼 공이 생성될때와 파괴될때를 코드안에서 찾아야 하는데 

     

    우선 생성되는것은 같은 코드내에 존재한다. 아이템을 날리든, 기본구체를 날리든 생성한뒤

    AddBall() 메서드만 실행해주면 되는것이다

     

    하지만 위 코드는 플레이어가 발사하는 공만 포함된다. 적 유닛이 발사하는 구체까지 포함하려면

    코드를 더 추가해줘야 한다

     

    우선 적 유닛의 발사를 담당하는 Enemy1center 스크립트에 SPGameManger를 추가해주자

     그런뒤 발사를 명령하는 enemy1Fire.SpawnBullet() 을 실행하기전에 AddBall 메서드를 실행해준다

    그럼 이런 유닛은 어떻게 되는걸까? 한번에 하나의 구체를 발사하는것이 아닌 4개의 구체를 동시에 발사하는 유닛이다

     

    상관없다. 모든 적 유닛은 총구의 개수에 맞게 muzzle이 구성되어있다

    그리고 코드를 다시보자. foreach문 안에 모든 총구의 개수(enemy1fires)에서 총알이 발사되도록 반복한다

     

    그렇기 때문에 발사(enemy1fire.SpawnBullet)할때마다 AddBall()을 실행해주면 되는것이다

     

    그럼 공이 언제 파괴될까? 그건 

     private void OnCollisionEnter2D(Collision2D coll)
        {
            if (!hasExpanded)
            {
                bGMControl.SoundEffectPlay(0);
            }
            if ((coll.gameObject.tag == "P1ball" || coll.gameObject.tag == "P2ball" || coll.gameObject.tag == "P1Item" || coll.gameObject.tag == "P2Item"
                || coll.gameObject.tag == "EnemyBall" || coll.gameObject.tag == "Item") && rigid == null)
            {
                if (randomNumber > 0)
                {
                    randomNumber--;
                    textMesh.text = randomNumber.ToString();
                }
                if (randomNumber <= 0)
                {
                    Destroy(gameObject);
                }
            }
            if ((coll.gameObject.name == "SPTwiceF(Clone)" && rigid == null )|| (coll.gameObject.name == "TwiceBullet(Clone)" && rigid == null))
            {
                randomNumber -= 1;
                if (randomNumber > 0)
                {
                    textMesh.text = randomNumber.ToString();
                }
                if (randomNumber <= 0)
                {
                    Destroy(gameObject);
                }
            }

     

    여기 존재한다. 정확히는 구체를 컨드롤하는 BallController 스크립트의 일부이다

    코드를 보면 구체의 내구도(randomNumber)가 0보다 작거나 같아지는 순간 파괴되는것을 볼 수 있다

     

    그러니 이때 RemoveBall() 메서드를 넣어줘야 한다. 이것도 어려울 것 없다

    그냥 파괴(Destroy(gameobject))되기 전에 RemoveBall() 메서드를 실행해주기만 하면 된다

     

    변경된 코드

     

     

    이제 정상적으로 작동하는것을 볼 수 있다. 생성되면 1이 늘어나고, 파괴되면 파괴된만큼 수가 줄어든다

     

    눈에 쉽게 보기 위해 update문에 로그를 찍어 직접 totalBalls의 수를 실시간으로 볼 수 있게 만들었다

    'Galaxy Ball > 5. 최적화' 카테고리의 다른 글

    최적화 - 6  (1) 2024.09.24
    최적화 - 5 (BallController 스크립트 제작#2)  (0) 2024.09.20
    최적화 - 4 (BallController 스크립트 제작#1)  (1) 2024.09.20
    최적화 - 3  (1) 2024.09.19
    최적화 - 2  (1) 2024.09.10
Designed by Tistory.