Galaxy Ball/5. 최적화

최적화 - 13 (코드 수정 #SmoothDamp, #LateUpdate, #Awake)

DOlpa_GB 2024. 11. 5. 01:58

계속해서 코드 수정을 이어나가보겠다

 

이번글에서는 적 유닛에 대한 코드 수정을 이어나가보겠다

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public class EnemyBulletControl : MonoBehaviour
{
    SPGameManager spGameManager;
    Rigidbody2D rb;
    BGMControl bGMControl;
    Vector2 lastVelocity;
    float deceleration = 2f;
    public float increase;
    private bool iscolliding = false;
    public bool hasExpanded = false;
    private bool isStopped = false;
    private float decelerationThreshold = 0.4f;
    public int PlusScale;
    private float expandSpeed = 1f; // 팽창 속도

    private int durability;
    private TextMeshPro textMesh;
    public float fontsize;
    public int BallMinHP = 1;
    public int BallMaxHP = 6;
    public PhysicsMaterial2D bouncyMaterial;
    private Vector3 initialScale; // 초기 공 크기
    private Vector3 targetScale; // 목표 크기
    private const string SPTwiceFName = "SPTwiceF(Clone)";
    private const string TwiceBulletName = "TwiceBullet(Clone)";
    private const string GojungTag = "Gojung";
    private const string WallTag = "Wall";
    private const string EnemyCenterTag = "EnemyCenter";
    public bool isExpanding = false; // 공이 팽창 중인지 여부



    private void Start()
    {
        spGameManager = FindAnyObjectByType<SPGameManager>();
        bGMControl = FindAnyObjectByType<BGMControl>();
        rb = GetComponent<Rigidbody2D>();
        GameObject textObject = new GameObject("TextMeshPro");

        textObject.transform.parent = transform;
        textMesh = textObject.AddComponent<TextMeshPro>();
        durability = Random.Range(BallMinHP, BallMaxHP);
        textMesh.text = durability.ToString();
        textMesh.fontSize = fontsize;
        textMesh.alignment = TextAlignmentOptions.Center;
        textMesh.autoSizeTextContainer = true;
        textMesh.rectTransform.localPosition = Vector3.zero;
        textMesh.sortingOrder = 1;

        rb.drag = 0.1f;
        rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
        rb.interpolation = RigidbodyInterpolation2D.Interpolate;

        Collider2D collider = GetComponent<Collider2D>();
        if (collider != null && bouncyMaterial != null)
        {
            collider.sharedMaterial = bouncyMaterial;
        }

        initialScale = transform.localScale;
    }

    private void Update()
    {
        if (!isStopped)
        {
            SlowDownBall();
        }

        if (isExpanding)
        {
            ExpandBall();
        }
    }

    void SlowDownBall()
    {
        if (rb == null) return;

        rb.velocity *= 1f - (Time.deltaTime * (deceleration * 0.4f)); // drag 효과 줄이기
        if (rb.velocity.magnitude <= decelerationThreshold)
        {
            rb.velocity = Vector2.zero;
            isStopped = true;
            StartExpansion();
        }
    }



    void StartExpansion()
    {
        if (bGMControl.SoundEffectSwitch)
        {
            bGMControl.SoundEffectPlay(1);
        }
        targetScale = initialScale * PlusScale;
        isExpanding = true;
    }

    void ExpandBall()
    {
        if (Vector3.Distance(transform.localScale, targetScale) > 0.01f)
        {
            transform.localScale = Vector3.Lerp(transform.localScale, targetScale, Time.deltaTime * expandSpeed);
        }
        else
        {
            transform.localScale = targetScale; // 목표 크기에 도달하면 팽창 완료
            isExpanding = false; // 팽창 중단
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (!isExpanding && bGMControl.SoundEffectSwitch)
        {
            bGMControl.SoundEffectPlay(0);
        }
        if (!collision.collider.isTrigger && isExpanding)
        {
            isExpanding = false; // 팽창 중단
            transform.localScale = transform.localScale; // 현재 크기에서 멈춤
            DestroyRigidbody(); // Rigidbody 제거
        }

        if ((collision.collider.name != SPTwiceFName || collision.collider.name != TwiceBulletName) && rb == null)
        {
            if (collision.collider.CompareTag(GojungTag)) return;
            if (collision.collider.CompareTag(WallTag)) return;

            TakeDamage(1);
            textMesh.text = durability.ToString();
        }
        if ((collision.collider.name == SPTwiceFName || collision.collider.name == TwiceBulletName) && rb == null)
        {
            TakeDamage(2);
            textMesh.text = durability.ToString();
        }
    }
    void TakeDamage(int damage)
    {
        durability -= damage;
        if (durability <= 0)
        {
            spGameManager.RemoveBall();
            Destroy(gameObject);
        }
    }
    void DestroyRigidbody()
    {
        if (rb != null)
        {
            Destroy(rb);
            rb = null;
        }
    }
}

 

가장 먼저 적 유닛이 날리는 총알을 컨트롤하는 스크립트를 전부 수정해주었다

이렇게 넘어가려고 했으나 최근에 FixedUpdate에 대해 알게 되면서 update > FixedUpdate로 수정해주었으나..

 

매 프레임마다 업데이트를 해주지 않기 때문에 공이 팽창할때마다 뚝뚝 끊겨보이는것을 확인할 수 있었다

이것에 대한 대안으로써는 Lerp 조절, Smooth Damping 사용, Update와 FixedUpdate 혼합 사용이 있었다

 

난 이중에서 Smooth Damping을 사용하기로 했다. 

 

transform.localScale = Vector3.SmoothDamp(transform.localScale, targetScale, ref velocity, expandSpeed);

 

수정 방법은 간단하다. 공을 팽창시키는 Lerp를 SmoothDamp를 사용해주면 된다

하지만 FixedUpdate안에 이걸 넣어주다 보니 SmoothDamp를 사용해도 큰 변화는 없다는것..

그래서 어쩔수 없이 Update와 FixedUpdate 혼합 사용까지 해주었다

  private void FixedUpdate()
    {
        if (!isStopped)
        {
            SlowDownBall();
        }
    }
    private void Update()
    {
        if (isExpanding)
        {
            ExpandBall();
        }
    }
    
    ....
    
    
     void ExpandBall()
    {
        transform.localScale = Vector3.SmoothDamp(transform.localScale, targetScale, ref velocity, expandSpeed);

        if (Vector3.Distance(transform.localScale, targetScale) < 0.01f)
        {
            transform.localScale = targetScale; // 목표 크기에 도달하면 팽창 완료
            isExpanding = false; // 팽창 중단
        }
    }

 

오...이러니까 정말 신기해졌다. 영상 녹화는 프레임 제한이 있어 차이를 느낄수 없지만

 

예전에는 1.정지 2.팽창 이런 느낌으로 단계가 확실하게 끊겨있는 느낌이라면

지금은 1. 정지&팽창 느낌으로 둘의 경계가 모호할만큼 굉장히 부드럽게 팽창을 한다

 

하지만 Lerp보다 SmoothDamp는 더 많은 연산을 필요로 한다는것.. 성능이 중요하다면 Lerp가 맞다

하지만 둘의 차이가 거의 없으므로 일단 SmoothDamp를 사용하도록 하겠다

 

using System.Collections;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;

public class Enemy1center : MonoBehaviour
{
    SPGameManager spGameManager;
    BGMControl bGMControl;
    Rigidbody2D rigid;
    public float increase = 4f;
    public bool hasExpanded = false;
    public int randomNumber;
    public int initialRandomNumber; // 초기 randomNumber 값을 저장할 변수
    public TextMeshPro textMesh;
    public Enemy1Fire[] enemy1Fires; // 여러 Enemy1Fire 참조를 위한 배열
    public bool isShowHP;
    public bool isHide;

    public int MaxHP;
    public int MinHP;
    public float MaxFireTime;
    public float MinFireTime;
    public float MaxAngle;
    public float MinAngle;
    public float fontsize;

    private void Start()
    {
        spGameManager = FindObjectOfType<SPGameManager>();
        bGMControl = FindObjectOfType<BGMControl>();
        rigid = GetComponent<Rigidbody2D>();
        GameObject textObject = new GameObject("TextMeshPro");
        textObject.transform.parent = transform;
        textMesh = textObject.AddComponent<TextMeshPro>();
        randomNumber = Random.Range(MinHP, MaxHP);
        initialRandomNumber = randomNumber; // 초기 randomNumber 값을 저장
        if (isShowHP)
        {
            textMesh.text = randomNumber.ToString();
        }
        textMesh.fontSize = fontsize;
        textMesh.alignment = TextAlignmentOptions.Center;
        textMesh.autoSizeTextContainer = true;
        textMesh.rectTransform.localPosition = Vector3.zero;
        textMesh.sortingOrder = 3;

        enemy1Fires = GetComponentsInChildren<Enemy1Fire>(); // Enemy1Fire 컴포넌트 배열 참조

        StartCoroutine(RotateObject());

        if (isHide)
        {
            ChangeObjectColor(transform);
        }
    }

    private void OnCollisionEnter2D(Collision2D coll)
    {
        if (coll.gameObject.tag == "P1ball" || coll.gameObject.tag == "P2ball" || coll.gameObject.tag == "P1Item" || coll.gameObject.tag == "P2Item"
            || (coll.gameObject.tag == "Item" && coll.gameObject.name != "SPEndlessF(Clone)"))
        {
            if (randomNumber > 0)
            {
                randomNumber--;
                if (isShowHP)
                {
                    textMesh.text = randomNumber.ToString();
                }
            }
            if (randomNumber <= 0)
            {
                bGMControl.SoundEffectPlay(4);
                spGameManager.RemoveEnemy();

                Destroy(transform.parent.gameObject); // 부모 오브젝트 삭제
            }
        }
        if (coll.gameObject.name == "SPTwiceF(Clone)")
        {
            randomNumber -= 1;
            if (randomNumber > 0)
            {
                textMesh.text = randomNumber.ToString();
            }
            if (randomNumber <= 0)
            {
                bGMControl.SoundEffectPlay(4);
                spGameManager.RemoveEnemy();

                Destroy(gameObject);
            }
        }
    }
    private IEnumerator RotateObject()
    {
        while (true)
        {
            // 5초 동안 정지
            yield return new WaitForSeconds(Random.Range(MinFireTime, MaxFireTime));

            // 회전할 각도 설정
            float targetAngle = Random.Range(MinAngle, MaxAngle);
            float currentAngle = transform.eulerAngles.z;
            float rotationTime = 1f; // 회전하는 데 걸리는 시간
            float elapsedTime = 0f;

            // 회전하기
            while (elapsedTime < rotationTime)
            {
                elapsedTime += Time.deltaTime;
                float angle = Mathf.LerpAngle(currentAngle, targetAngle, elapsedTime / rotationTime);
                transform.eulerAngles = new Vector3(0, 0, angle);
                yield return null;
            }

            // 회전이 끝난 후 1초 뒤에 총알 발사
            yield return new WaitForSeconds(1f);

            if (enemy1Fires != null)
            {
                foreach (var enemy1Fire in enemy1Fires)
                {
                    spGameManager.AddBall();
                    enemy1Fire.SpawnBullet();
                }
            }
        }
    }

    private void ChangeObjectColor(Transform parentTransform)
    {
        if (!isHide)
            return;

        // 자기 자신의 색상 변경
        SpriteRenderer currentSpriteRenderer = parentTransform.GetComponent<SpriteRenderer>();
        if (currentSpriteRenderer != null)
        {
            GameObject background = GameObject.Find("BackGround");
            if (background != null)
            {
                SpriteRenderer backgroundSpriteRenderer = background.GetComponent<SpriteRenderer>();
                if (backgroundSpriteRenderer != null)
                {
                    currentSpriteRenderer.color = backgroundSpriteRenderer.color;
                }
            }
        }

        // 부모 오브젝트의 자식들을 검사하면서 색상을 변경
        foreach (Transform child in parentTransform)
        {
            ChangeObjectColor(child);
        }
    }
}

 

그 다음은 적 유닛의 몸통 부분을 컨트롤 하는 코드인데...확실히 가독성도 떨어지고 최적화가 필요해보인다

 

제일 먼저 눈에 들어오는건

  private void OnCollisionEnter2D(Collision2D coll)
    {
        if (coll.gameObject.tag == "P1ball" || coll.gameObject.tag == "P2ball" || coll.gameObject.tag == "P1Item" || coll.gameObject.tag == "P2Item"
            || (coll.gameObject.tag == "Item" && coll.gameObject.name != "SPEndlessF(Clone)"))
        {
            if (randomNumber > 0)
            {
                randomNumber--;
                if (isShowHP)
                {
                    textMesh.text = randomNumber.ToString();
                }
            }
            if (randomNumber <= 0)
            {
                bGMControl.SoundEffectPlay(4);
                spGameManager.RemoveEnemy();

                Destroy(transform.parent.gameObject); // 부모 오브젝트 삭제
            }
        }
        if (coll.gameObject.name == "SPTwiceF(Clone)")
        {
            randomNumber -= 1;
            if (randomNumber > 0)
            {
                textMesh.text = randomNumber.ToString();
            }
            if (randomNumber <= 0)
            {
                bGMControl.SoundEffectPlay(4);
                spGameManager.RemoveEnemy();

                Destroy(gameObject);
            }
        }
    }

 

이 복잡한 충돌 계산들... 너무나도 많은 태그와 이름비교 연산들이 난무한다.

이것들도 총알 충돌처리와 똑같이 최대한 간결하고 TakeDamage 메서드 방식을 활용해보겠다

 

private void OnCollisionEnter2D(Collision2D coll)
    {
        if (coll.gameObject.tag == "EnemyBall") return;
        if (coll.gameObject.name != SPEndlessFName)
        {
            TakeDamage(1);
        }
        if (coll.gameObject.name == SPTwiceFName)
        {
            TakeDamage(1);
        }
    }
    void TakeDamage(int damage)
    {
        durability -= damage;
        if (isShowHP)
        {
            textMesh.text = durability.ToString();
        }
        if (durability <= 0)
        {
            bGMControl.SoundEffectPlay(4);
            spGameManager.RemoveEnemy();
            Destroy(gameObject);
        }
    }

 

최대한 간결하게 변경해주었다

 

 private IEnumerator RotateObject()
    {
        while (true)
        {
            // 랜덤한 대기 시간
            yield return new WaitForSeconds(Random.Range(MinFireTime, MaxFireTime));

            // 회전할 각도 설정
            float targetAngle = Random.Range(MinAngle, MaxAngle);
            float currentAngle = transform.eulerAngles.z;
            float rotationTime = 1f; // 회전하는 데 걸리는 시간
            float elapsedTime = 0f;

            // 회전하기
            while (elapsedTime < rotationTime)
            {
                elapsedTime += Time.deltaTime;
                float angle = Mathf.LerpAngle(currentAngle, targetAngle, elapsedTime / rotationTime);
                transform.eulerAngles = new Vector3(0, 0, angle);
                yield return null;
            }

            // 회전이 끝난 후 총알 발사
            FireBullets();
        }
    }

    // 총알 발사 메서드
    private void FireBullets()
    {
        if (enemy1Fires != null)
        {
            foreach (var enemy1Fire in enemy1Fires)
            {
                spGameManager.AddBall();
                enemy1Fire.SpawnBullet();
            }
        }
    }

 

총구 회전과 총알 발사 메서드도 수정해주었다. FireBullets 메서드를 따로 분류하고 불필요한 호출을 줄였다

 

이외에도 꽤 많은 적 유닛이나 총알에 대한 최적화가 이루어졌으나 하나하나 코드를 다 설명할수는 없어

중요한 핵심코드만 짚고 넘어가겠다

 

 

그 다음은 카메라를 컨트롤 하는 CameraControl이다

using UnityEngine;

public class CameraControl : MonoBehaviour 
{
    GameObject player;
    GameObject mainPlayer;
    public RectTransform stage1RectTransform;
    public Camera mainCamera;

    void Start()
    {
        this.player = GameObject.Find("Player");
        this.mainPlayer = GameObject.Find("Main Player");
    }

    void Update()
    {
        Vector3 targetPos;
        
        if (mainPlayer != null)
        {
            targetPos = mainPlayer.transform.position;
            transform.position = new Vector3(targetPos.x, targetPos.y + 1.1f, transform.position.z);
        }
        else if (player != null)
        {
            targetPos = player.transform.position;
            transform.position = new Vector3(targetPos.x, targetPos.y + 5, transform.position.z);
        }
        else
        {
            return;
        }

    }
}

이 코드를

using UnityEngine;

public class CameraControl : MonoBehaviour 
{
    private GameObject player;
    private GameObject mainPlayer;
    public Camera mainCamera;

    private Vector3 offsetMainPlayer = new Vector3(0, 1.1f, 0);
    private Vector3 offsetPlayer = new Vector3(0, 5, 0);

    void Awake()
    {
        player = GameObject.Find("Player");
        mainPlayer = GameObject.Find("Main Player");
    }

    void LateUpdate()
    {
        Vector3 targetPos;

        if (mainPlayer != null)
        {
            targetPos = mainPlayer.transform.position + offsetMainPlayer;
            transform.position = new Vector3(targetPos.x, targetPos.y, transform.position.z);
        }
        else if (player != null)
        {
            targetPos = player.transform.position + offsetPlayer;
            transform.position = new Vector3(targetPos.x, targetPos.y, transform.position.z);
        }
        // No else needed; simply return if both players are null.
    }
}

 

이렇게 최적화 해주었다. 우선 변할일이 없는 z축은 완전 고정해주었고

카메라 움직임에 적합한 LateUpdate를 사용해 더 부드러운 움직임을 구현하도록 하였다

더보기

Update

  • 장점:
    • 즉각적인 반응: 입력 처리 및 게임 로직을 처리하는 데 적합하여, 사용자 입력에 대한 즉각적인 반응이 필요할 때 사용됩니다.
    • 연속성: 프레임마다 호출되기 때문에, 부드러운 애니메이션이나 게임의 진행을 위한 작업에 적합합니다.
  • 단점:
    • 성능 부담: 많은 로직을 Update에 배치하면, 매 프레임마다 호출되므로 성능에 부담을 줄 수 있습니다. 특히 CPU-intensive한 작업은 피하는 것이 좋습니다.

LateUpdate

  • 장점:
    • 정확한 위치 조정: 모든 물체의 업데이트가 완료된 후 호출되므로, 카메라 같은 객체가 정확한 위치를 조정하는 데 유리합니다. 이는 특히 카메라 추적 시스템이나 애니메이션 시스템에 중요합니다.
    • 불필요한 계산 감소: 위치나 상태를 Update에서 처리하고 LateUpdate에서 조정하면, 매 프레임의 위치 업데이트에 따라 추가 계산을 줄일 수 있습니다.
  • 단점:
    • 타이밍 의존성: 만약 특정 로직이 LateUpdate에서 처리되어야 한다면, 다른 오브젝트와의 동기화를 고려해야 합니다. 이는 상황에 따라 복잡해질 수 있습니다.

 

 

그리고 이건 처음 알게 된건데 Start와 Awake는 호출 순서에 따라 성능에 영향을 줄 수 있다고 한다

늘 둘이 무슨 차이인건지 항상 애매했었는데 이 참에 알아보자

 

더보기

Awake 메서드

  • 호출 시기: Awake는 MonoBehaviour가 인스턴스화될 때 호출됩니다. 즉, 게임 오브젝트가 활성화되기 전에 실행되며, 해당 오브젝트의 모든 컴포넌트가 초기화된 후에 호출됩니다.
  • 초기화 용도: 게임 오브젝트가 활성화되기 전에 필요한 초기 설정이나 변수를 초기화하는 데 사용됩니다.
  • 성능: Awake는 일반적으로 게임 오브젝트가 생성되자마자 호출되므로, 초기화 시 필요한 작업(예: 변수 설정, 이벤트 구독 등)을 미리 처리할 수 있습니다. GameObject.Find와 같은 함수는 자주 호출하면 성능에 영향을 줄 수 있으므로, Awake에서 호출하여 한 번만 처리하는 것이 효율적입니다.

Start 메서드

  • 호출 시기: Start는 해당 게임 오브젝트가 활성화된 후 프레임의 처음에 호출됩니다. 즉, 모든 Awake가 호출된 후에 실행됩니다.
  • 게임 로직 시작: 게임이 시작할 때 필요한 초기화나 로직을 처리하는 데 적합합니다. 예를 들어, 외부 리소스나 다른 게임 오브젝트와의 의존성을 설정할 때 유용합니다.
  • 성능: Start 메서드는 Awake 이후에 호출되므로, 해당 오브젝트가 활성화될 때만 실행됩니다. 따라서 초기화가 필요한 모든 요소가 준비된 후에 호출되므로, 의존성이 있는 초기화에 적합합니다.

성능 차이

  1. 호출 순서: Awake는 객체 생성 시에 호출되므로, 오브젝트가 활성화되지 않아도 초기화가 필요할 때 사용됩니다. 이는 특히 객체가 여러 번 생성되거나 비활성화 및 재활성화 되는 경우 유용합니다.
  2. 객체 활성화: Start는 해당 오브젝트가 활성화된 후 호출되기 때문에, 다른 오브젝트와의 관계 설정이 필요할 때 더 적합합니다. 만약 여러 오브젝트의 Start 메서드가 서로 의존적이라면, Awake를 사용하여 초기화를 처리하는 것이 좋습니다.
  3. 성능 고려: GameObject.Find는 비효율적이기 때문에, Awake에서 한 번만 호출하여 초기화하는 것이 성능에 더 유리합니다. 자주 호출하면 성능에 부정적인 영향을 미칠 수 있으므로, 가능한 초기화 시점에서 호출하도록 하는 것이 좋습니다.

결론

  • 최적화: Awake를 사용하여 GameObject.Find와 같은 초기화 작업을 수행하는 것은 성능 최적화에 도움이 됩니다. 여러 번 호출될 필요 없이 한 번만 초기화하므로, 전체적인 성능이 향상됩니다.
  • 사용 상황: 일반적으로 게임 오브젝트의 상태나 변수를 초기화할 필요가 있는 경우 Awake를 사용하고, 게임 로직이나 다른 컴포넌트와의 관계를 설정할 필요가 있을 때는 Start를 사용합니다.

진짜 오랜만에 보는 미니게임 씬... Interpolate 설정도 해주고

 

using UnityEngine;
using UnityEngine.SceneManagement;

public class MovingSphere : MonoBehaviour
{
    public float speed = 20f;
    public float moveDuration = 0.1f; // 이동 시간
    private Vector2 moveDirection;
    private float moveTimer = 0f;
    private Rigidbody2D rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        SetRandomDirection();
    }

    private void Update()
    {
        moveTimer += Time.deltaTime;

        // Move the Rigidbody
        rb.MovePosition(rb.position + moveDirection * speed * Time.deltaTime);

        if (moveTimer >= moveDuration)
        {
            SetRandomDirection();
            moveTimer = 0f;
        }
    }

    private void OnMouseDown()
    {
        // Optionally implement an async scene loading here if needed
        SceneManager.LoadScene("Start Scene");
    }

    private void SetRandomDirection()
    {
        float randomAngle = Random.Range(0f, 360f);
        moveDirection = Quaternion.Euler(0, 0, randomAngle) * Vector2.right;
    }
}

 

계속해서 빠르게 움직여야 하는 공인데 이동을 Transform.Translate로 하길래

RigidBody2D의 Moveposition으로 이동하도록 변경해주었다

반응형