-
최적화 - 3Galaxy Ball/5. 최적화 2024. 9. 19. 19:41
사실 나도 어떤 부분을 손대야 게임이 부드럽게 구현되는지는 알 수 없다.
그러니 될 때까지 의심가는 부분들을 전부 하나씩 손을 봐가며 진행할 뿐...
이번엔 구체를 직접 컨트롤하는 BallController 스크립트를 최적화 해보도록 하겠다
사실 게임을 모바일에서 실행했을때 프레임이 끊긴다는 것을 알 수 있었던 이유는 구체 때문이다
구체를 발사하는데 유니티에서 본 것처럼 부드럽게 나가는것이 아니었기에 가장 큰 문제는 어쩌면 이 코드일수도 있다
using System.Collections; using TMPro; using UnityEngine; public class BallController : MonoBehaviour { Rigidbody2D rigid; Vector2 lastVelocity; float deceleration = 2f; public float increase = 4f; private bool iscolliding = false; public bool hasExpanded = false; private bool isStopped = false; private int randomNumber; private TextMeshPro textMesh; private bool hasBeenReleased = false; public float fontsize; BGMControl bGMControl; SPGameManager spGameManager; 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(1, 6); textMesh.text = randomNumber.ToString(); textMesh.fontSize = fontsize; textMesh.alignment = TextAlignmentOptions.Center; textMesh.autoSizeTextContainer = true; textMesh.rectTransform.localPosition = Vector3.zero; textMesh.sortingOrder = 1; } private void Update() { // 최초 클릭 이후에만 힘이 가해지도록 설정 if (Input.GetMouseButtonUp(0) && rigid != null && !hasBeenReleased) { rigid.velocity = GameManager.shotDirection * GameManager.shotDistance; // GameManager에서 값 가져와서 구체 발사 hasBeenReleased = true; // 최초 클릭이 되었음을 표시 } Move(); expand(); } void Move() { if (rigid == null || isStopped) return; lastVelocity = rigid.velocity; rigid.velocity -= rigid.velocity.normalized * deceleration * Time.deltaTime; if (rigid.velocity.magnitude <= 0.01f && hasExpanded) { isStopped = true; StartCoroutine(DestroyRigidbodyDelayed()); } } void expand() { if (rigid == null || iscolliding) return; if (rigid.velocity.magnitude > 0.1f) return; if (Input.GetMouseButton(0)) return; if (!hasExpanded) { bGMControl.SoundEffectPlay(1); } transform.localScale += Vector3.one * increase * Time.deltaTime; hasExpanded = true; } 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) { spGameManager.RemoveBall(); 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) { spGameManager.RemoveBall(); Destroy(gameObject); } } if (coll.contacts != null && coll.contacts.Length > 0) { Vector2 dir = Vector2.Reflect(lastVelocity.normalized, coll.contacts[0].normal); if (rigid != null) rigid.velocity = dir * Mathf.Max(lastVelocity.magnitude, 0f); // 감속하지 않고 반사만 진행 } this.iscolliding = true; } private void OnCollisionExit2D(Collision2D collision) { this.iscolliding = false; } IEnumerator DestroyRigidbodyDelayed() { yield return new WaitForSeconds(0.8f); if (rigid != null) Destroy(rigid); } }
우선 문제의 BallController 코드이다. 그냥 딱 봐도 뭔가 잘못됐다는것을 알 수 있다
- TextMeshPro 생성: Start 메서드에서 매번 TextMeshPro를 동적으로 생성하고 있습니다. 이 과정이 많은 오브젝트에서 반복될 경우 성능에 부담이 될 수 있습니다.
- 해결 방안: TextMeshPro를 동적으로 생성하는 대신, 미리 만들어진 TextMeshPro 객체를 재사용하거나 오브젝트 풀링(Object Pooling)을 사용하세요.
- Rigidbody의 지속적인 계산: Move 메서드에서 매 프레임마다 속도 감소를 계산하고 있습니다. 또한 충돌 시 반사 계산도 들어가는데, 이러한 계산이 빈번하게 일어나면 성능에 영향을 줄 수 있습니다.
- 해결 방안: Rigidbody 관련 연산을 최적화하거나, 불필요한 물리 계산을 줄이는 방향으로 수정해야 할 수 있습니다.
- Physics 관련 충돌 처리: OnCollisionEnter2D 메서드에서 충돌 시 속도 반사 계산을 하고 있는데, 여러 오브젝트가 동시에 충돌하면 많은 계산이 발생할 수 있습니다. 이는 특히 Rigidbody와 함께 사용될 때 물리 엔진에 큰 부하를 줄 수 있습니다.
- 해결 방안: 충돌 처리 로직을 최대한 간소화하거나, 충돌 빈도를 줄이는 방법을 고려해보세요.
- 코루틴 사용: DestroyRigidbodyDelayed에서 Rigidbody를 0.8초 후에 삭제하는 코루틴이 동작하고 있습니다. 이런 코루틴이 여러 개 동시에 실행되면 메모리와 성능에 부담이 될 수 있습니다.
- 해결 방안: 코루틴 대신 타이머나 다른 방식으로 일정 시간 후 오브젝트를 삭제하는 것을 고려할 수 있습니다.
- Update 메서드: Update 메서드에서 매 프레임마다 여러 연산이 발생하고 있습니다. 특히 rigid.velocity의 연산과 Move, expand 메서드에서 매 프레임마다 조건을 검사하는 방식은 최적화에 걸림돌이 될 수 있습니다.
- 해결 방안: 조건 체크와 연산을 최소화하고, 필요할 때만 실행되도록 변경하는 게 좋습니다.
챗 지피티에서는 총 5가지의 문제점을 꼽아주었다. 이제 이것을 하나씩 하나씩 해결해나가보도록 하자
우선 1번째인 TextMeshPro.
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(1, 6); textMesh.text = randomNumber.ToString(); textMesh.fontSize = fontsize; textMesh.alignment = TextAlignmentOptions.Center; textMesh.autoSizeTextContainer = true; textMesh.rectTransform.localPosition = Vector3.zero; textMesh.sortingOrder = 1; }
매 구체마다 Start 메서드에서 이걸 하나하나 다 설정하고 넘어간다는게 굉장히 비효율적인것 같아 보이긴했다
그리고 사실 예전에 가르쳐주던 선생님께서 이 부분에 대해서는 설명해주신게 있다
결국 이렇게 복잡하게 코드를 짠 이유는 딱 하나. randomNumber 즉, 구체의 내구도 시스템이다
구체마다 내구도를 랜덤으로 부여하여 충돌이 일어날때마다 내구도가 줄어들도록 해주는것이다
이 내구도 시스템을 지금까지는 TextMeshPro로 넣어주었는데 선생님께서는 그럴 필요없이
내구도가 1,2,3,4,5인 이미지를 5장 준비해준뒤 충돌할때마다 이미지만 바꿔주라는 것이었다
그때 당시엔 개발에 전혀 문제도 없었고, 최적화같은건 생각도 안하고 있었으니 한 귀로 흘렸었다
하지만 이젠 그 조언을 받아들여야할 때가 온 것 같다
우선 각 내구도에 쓰일 이미지 5장을 준비해주자
이미지는 대략 만들었지만 이제 중요한건 사방에 보이는 저 흰색 배경을 제거해주어야 한다
원래는 이미지 누끼를 딸 때마다
이 홈페이지를 이용해주었지만, 한가지 단점이 있다면
고화질 다운로드를 받기 위해선 돈을 지불해야 한다는것.. 원래는 크게 신경쓰지 않았지만
이제부터는 화질이 굉장히 중요하기 때문에 이 방법을 사용할 수 없다.
그렇기 때문에 찾은 또다른 방법은 바로 3D 그림판을 이용하는것이다
이미지 배경제거 방법은 위 링크에 기재해두었다
그렇게 배경까지 제거한 이미지들. 이제 충돌이 일어날때마다
TextMeshPro에서 숫자가 깎이는 대신 이미지를 변경시켜주겠다
using System.Collections; using UnityEngine; public class BallController : MonoBehaviour { public Sprite[] numberSprites; // 숫자 스프라이트 배열 private void Start() { spGameManager = FindObjectOfType<SPGameManager>(); bGMControl = FindObjectOfType<BGMControl>(); rigid = GetComponent<Rigidbody2D>(); spriteRenderer = GetComponent<SpriteRenderer>(); randomNumber = Random.Range(1, 6); spriteRenderer.sprite = numberSprites[randomNumber - 1]; AdjustSpriteScale(); } void AdjustSpriteScale() { // 이미지의 픽셀 단위 크기 Vector2 spriteSize = spriteRenderer.sprite.bounds.size; // 원하는 크기에 맞게 스프라이트 크기를 변환 (transform.localScale을 사용) Vector2 prefabSize = transform.localScale; // 스프라이트의 사이즈를 프리팹의 사이즈에 맞춰서 조정 float scaleFactorX = prefabSize.x / spriteSize.x; float scaleFactorY = prefabSize.y / spriteSize.y; // 스프라이트의 크기를 맞춰줌 spriteRenderer.transform.localScale = new Vector3(scaleFactorX, scaleFactorY, 1f); } private void Update() { if (Input.GetMouseButtonUp(0) && rigid != null && !hasBeenReleased) { rigid.velocity = SPGameManager.shotDirection * SPGameManager.shotDistance; hasBeenReleased = true; } Move(); expand(); } void Move() { if (rigid == null || isStopped) return; lastVelocity = rigid.velocity; rigid.velocity -= rigid.velocity.normalized * deceleration * Time.deltaTime; if (rigid.velocity.magnitude <= 0.01f && hasExpanded) { isStopped = true; StartCoroutine(DestroyRigidbodyDelayed()); } } void expand() { if (rigid == null || iscolliding) return; if (rigid.velocity.magnitude > 0.1f) return; if (Input.GetMouseButton(0)) return; if (!hasExpanded) { bGMControl.SoundEffectPlay(1); } transform.localScale += Vector3.one * increase * Time.deltaTime; hasExpanded = true; } 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 > 1) { randomNumber--; // randomNumber 감소 spriteRenderer.sprite = numberSprites[randomNumber - 1]; // 스프라이트 업데이트 } else if (randomNumber <= 1) // randomNumber가 1 이하일 때 제거 { spGameManager.RemoveBall(); Destroy(gameObject); } } if ((coll.gameObject.name == "SPTwiceF(Clone)" && rigid == null) || (coll.gameObject.name == "TwiceBullet(Clone)" && rigid == null)) { if (randomNumber > 1) { randomNumber--; spriteRenderer.sprite = numberSprites[randomNumber - 1]; // 스프라이트 업데이트 } else if (randomNumber <= 1) { spGameManager.RemoveBall(); Destroy(gameObject); } }
그렇게 코드를 수정해주었으나....
하...도대체 뭐가 어디서부터 잘못된건지 모르겠다...
프리팹 원본과 내가 준비한 이미지의 사이즈가 달라서 이러는건가...?
-----------------------------------------------------------------------------
글에 다 담진 않았지만 여태까지 너무나도 많은 시행착오들이 많았고
그 결과 내가 내린 결론은 BallController는 이미 수정을 하기엔 너무 먼 길을 건너온 것 같다
그러니 아예 구체를 컨트롤하는 코드를 처음부터 다시 짜보도록 하겠다
늘 선생님께서 코드를 짤 때 가르쳐주던 순서가 있다. 물론 초보자용 설명이긴 했지만
가장 간단한 것, 쉬운것부터 차근차근 구현하나가는것이 중요하다는것 이었다
그러니 다시 초심으로 돌아가서 처음부터 코드를 세워나가보겠다
우선 코드를 싹 다 지우기 전에 기억해둬야 할게 하나 있다
바로 다른 코드에서 참조중인
BGMControl, SPGameManager 이 두가지이다.
첫번째에서는 효과음을 재생하고
두번째에서는 구체를 날릴 힘과 방향을 받아와 발사,
그리고 구체가 파괴될때마다 SPGameManager에게 정보를 전달해준다
이 세가지는 코드를 다시 짜더라도 똑같이 가져와야하는 부분이기 때문에 기억해둬야 한다
using System.Collections; using UnityEngine; public class BallController : MonoBehaviour { Rigidbody2D rigid; Vector2 lastVelocity; float deceleration = 2f; public float increase = 4f; private bool iscolliding = false; public bool hasExpanded = false; private bool isStopped = false; private int randomNumber; private bool hasBeenReleased = false; BGMControl bGMControl; SPGameManager spGameManager; private void Start() { spGameManager = FindObjectOfType<SPGameManager>(); bGMControl = FindObjectOfType<BGMControl>(); rigid = GetComponent<Rigidbody2D>(); randomNumber = Random.Range(1, 6); } private void Update() { if (Input.GetMouseButtonUp(0) && rigid != null && !hasBeenReleased) { rigid.velocity = SPGameManager.shotDirection * SPGameManager.shotDistance; hasBeenReleased = true; } Move(); expand(); } void Move() { if (rigid == null || isStopped) return; lastVelocity = rigid.velocity; rigid.velocity -= rigid.velocity.normalized * deceleration * Time.deltaTime; if (rigid.velocity.magnitude <= 0.01f && hasExpanded) { isStopped = true; StartCoroutine(DestroyRigidbodyDelayed()); } } void expand() { if (rigid == null || iscolliding) return; if (rigid.velocity.magnitude > 0.1f) return; if (Input.GetMouseButton(0)) return; if (!hasExpanded) { bGMControl.SoundEffectPlay(1); } transform.localScale += Vector3.one * increase * Time.deltaTime; hasExpanded = true; } 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 > 1) { randomNumber--; // randomNumber 감소 } else if (randomNumber <= 1) // randomNumber가 1 이하일 때 제거 { spGameManager.RemoveBall(); Destroy(gameObject); } } if ((coll.gameObject.name == "SPTwiceF(Clone)" && rigid == null) || (coll.gameObject.name == "TwiceBullet(Clone)" && rigid == null)) { if (randomNumber > 1) { randomNumber--; } else if (randomNumber <= 1) { spGameManager.RemoveBall(); Destroy(gameObject); } } if (coll.contacts != null && coll.contacts.Length > 0) { Vector2 dir = Vector2.Reflect(lastVelocity.normalized, coll.contacts[0].normal); if (rigid != null) rigid.velocity = dir * Mathf.Max(lastVelocity.magnitude, 0f); // 반사만 적용 } this.iscolliding = true; } private void OnCollisionExit2D(Collision2D collision) { this.iscolliding = false; } IEnumerator DestroyRigidbodyDelayed() { yield return new WaitForSeconds(0.8f); if (rigid != null) Destroy(rigid); } }
자 혹시 모르니까 코드 전체도 남겨놓겠다
TextMeshPro의 기능을 제거한 BallController 스크립트이다
혹시 몰라 깃허브에 저장도 오랜만에 한번 해주었다
게임에서는 내구도를 나타내는 숫자가 보이지 않을 예정이다
다음 글부터는 본격적으로 BallController 스크립트를 처음부터 짜보도록 하겠다.
'Galaxy Ball > 5. 최적화' 카테고리의 다른 글
최적화 - 6 (1) 2024.09.24 최적화 - 5 (BallController 스크립트 제작#2) (0) 2024.09.20 최적화 - 4 (BallController 스크립트 제작#1) (1) 2024.09.20 최적화 - 2 (1) 2024.09.10 최적화 - 1 (0) 2024.09.10 - TextMeshPro 생성: Start 메서드에서 매번 TextMeshPro를 동적으로 생성하고 있습니다. 이 과정이 많은 오브젝트에서 반복될 경우 성능에 부담이 될 수 있습니다.