-
최적화 - 5 (BallController 스크립트 제작#2)Galaxy Ball/5. 최적화 2024. 9. 20. 21:01
자 지난번 글에 이어 마저 코드를 작성해보도록 하자
우선 조금이라도 더 성능에 도움을 주기 위해 짜잘한 부분들을 수정해 주었다
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ExBallController : MonoBehaviour { SPGameManager spgamemanager; Rigidbody2D rb; bool hasBeenLaunched = false; bool isExpanding = false; // 공이 팽창 중인지 여부 bool isStopped = false; // 공이 완전히 멈췄는지 여부 private float decelerationThreshold = 0.4f; private float dragAmount = 1.1f; private float expandSpeed = 0.5f; // 팽창 속도 private Vector3 initialScale; // 초기 공 크기 private Vector3 targetScale; // 목표 크기 public PhysicsMaterial2D bouncyMaterial; // 공의 Collider2D에 적용할 물리 재질 void Start() { spgamemanager = FindAnyObjectByType<SPGameManager>(); rb = GetComponent<Rigidbody2D>(); rb.drag = 0f; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; rb.interpolation = RigidbodyInterpolation2D.Interpolate; // 물리 재질이 설정된 경우 Collider2D에 적용 Collider2D collider = GetComponent<Collider2D>(); if (collider != null && bouncyMaterial != null) { collider.sharedMaterial = bouncyMaterial; } initialScale = transform.localScale; // 초기 공 크기 저장 } void Update() { if (!hasBeenLaunched && !spgamemanager.isDragging) { LaunchBall(); } if (hasBeenLaunched && !isStopped) { SlowDownBall(); } if (isExpanding) { ExpandBall(); // 팽창이 진행 중이면 계속 실행 } } void LaunchBall() { Vector2 launchForce = SPGameManager.shotDirection * SPGameManager.shotDistance; rb.AddForce(launchForce, ForceMode2D.Impulse); rb.drag = dragAmount; hasBeenLaunched = true; } void SlowDownBall() { // Rigidbody2D가 삭제되었는지 확인 if (rb == null) return; if (rb.velocity.magnitude <= decelerationThreshold) { rb.velocity = Vector2.zero; // 공을 정지시킴 isStopped = true; StartExpansion(); // 팽창 시작 } } void StartExpansion() { targetScale = initialScale * 10f; // 팽창 목표 크기 설정 isExpanding = true; } void ExpandBall() { // 팽창이 완료되지 않은 경우에만 Lerp 실행 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) { // Trigger가 아닌 Collider와 충돌 시 팽창 멈추기 if (!collision.collider.isTrigger && isExpanding) { isExpanding = false; // 팽창 중단 transform.localScale = transform.localScale; // 현재 크기에서 멈춤 DestroyRigidbody(); // Rigidbody 제거 } } void DestroyRigidbody() { if (rb != null) { Destroy(rb); // Rigidbody2D 삭제 rb = null; // null로 설정하여 후속 연산 방지 } } }
현재 내가 가장 중요하게 생각하는 update 메서드를 최소화 해주었고
팽창이 완료된 후에는 Lerp 연산을 하지 않도록 막아주었다
6. 내구도
자 이번엔 내구도 기능을 추가해보도록 하자. 사실 내구도 기능 자체는 어렵지 않다.
다만 어떤 아이템과 충돌했냐에 따라 내구도를 줄이기도 하고,
늘리기도 하고, 아니면 아예 파괴해버리기도 하는것이 복잡할뿐...
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ExBallController : MonoBehaviour { SPGameManager spgamemanager; Rigidbody2D rb; private int durability; // 내구도 변수 void Start() { // 내구도를 1~5 사이의 랜덤 값으로 설정 durability = Random.Range(1, 6); } ... private void OnCollisionEnter2D(Collision2D collision) { // 내구도 감소 로직 if (collision.collider.CompareTag("P1ball")) { durability--; // 내구도 1 감소 Debug.Log("내구도 감소, 현재 내구도: " + durability); // 내구도가 0 이하가 되면 오브젝트를 파괴 if (durability <= 0) { Destroy(gameObject); return; } } } }
우선 durability 라는 내구도용 변수를 추가해준뒤 만약 충돌이 일어났을때 충돌한 오브젝트의 태그가 P1ball이라면
내구도를 1 감소시켜주고, 만약 내구도가 0과 같거나 작아진다면 공을 파괴시켜준다
하지만 한가지 찝찝한게 하나 있다. 내가 지금까지 열심히 최적화를 하면서 깨달은,
최적화에 치명적인 부분을 몇개 적어보자면
1. update 메서드 안에 너무 많은 코드
2. 과도한 Destroy & Instantiate 남발 (무거운 계산)
3. Tag 비교연산 남발 (무거운 계산)
4. 코드로 물리엔진 구현
이 정도로 늘 강조하는 부분들이다. 그중 태그 비교연산은 최대한 줄이는게 좋다고들 하는데
이번에도 태그 연산이 들어가는것....게다가 지금은 P1ball 하나만 임시로 넣어줬는데
앞으로 각종 아이템까지 연계하면 더더욱 많아질 예정이다
private const string P1ballTag = "P1ball"; private void OnCollisionEnter2D(Collision2D collision) { if (collision.collider.CompareTag(P1ballTag)) { TakeDamage(1); } }
그래서 캐싱이라는 방법을 사용해보았다.
충돌이 일어날때마다 태그를 불러와 읽는다기보다는 미리 string타입 변수 P1ballTag에 "P1ball"을 집어넣고
만약 태그를 비교하여 미리 정해둔 문자열과 맞다면 TakeDamage 메서드를 실행하는것이다
void TakeDamage(int damage) { durability -= damage; if (durability <= 0) { Destroy(gameObject); } }
거기에 한가지 조건을 더 추가해주었다. 만약 내구도가 1로 설정된 공은 벽을 제외한
어딘가에 부딫히는 순간 소멸하기 때문에 적어도 이동중에는 데미지를 입지 않도록 해보자
private void OnCollisionEnter2D(Collision2D collision) { // Rigidbody가 삭제된 상태일 때만 내구도 감소 if (rb == null && collision.collider.CompareTag(P1ballTag)) { TakeDamage(1); // 내구도 감소 처리 } }
간단하다. 데미지가 들어가는 조건에 rb==null을 추가해주기만 하면 되는것
그렇게 코드가 어느새 130줄이 넘어가버리고 나같은 초보자들은 긴장이 된다.
최적화를 위해 코드가 늘어난것인데, 정작 코드가 길어지는것도 최적화에 지장을 주지 않을까..?
상식적으로 공 하나에 이 긴 코드들이 하나씩 부착된다는것인데...프레임에 지장을 주지 않을까...?
하지만 이 부분은 챗gpt 친구가 친절하게 설명해주었다
현재 코드가 길더라도, 게임 내에서 공이 하나씩 생성될 때마다 적용되는 스크립트 자체는 길이가 성능에 직접적으로 영향을 주는 것이 아니에요. 스크립트의 효율성과 반복적인 연산의 최적화가 더 중요한 요소입니다.
그렇다고 한다. 맘 편하게 코드를 짜가도 되겠다. 또 한편으로는 처음 게임을 만들때 이런 부분들을
단1도 신경쓰지 않고 멋대로 만들었으니 프레임이 뚝뚝 끊겨버릴만도 하다
그리고 늘 고민중이던 오브젝트 풀링...은 안해도 될것같다. 어차피 공의 최대갯수는 16개까지이기 때문에
최적화에는 큰 지장이 없을것같다
7. 내구도 표시
사실 내구도 표시는 더 좋은 방법이 없을까 고민이 많았었다.
하지만 이런저런 시도를 해본결과 내가 가장 원하는 모양대로 나오는건 기존의 방식이고
무엇보다 기존 방식이 최적화에 큰 영향을 끼치지 않는것을 알고나서는 굳이 바꾸지 않기로 했다
void Start() { textMesh = textObject.AddComponent<TextMeshPro>(); textMesh.text = durability.ToString(); textMesh.fontSize = 4; textMesh.alignment = TextAlignmentOptions.Center; textMesh.autoSizeTextContainer = true; textMesh.rectTransform.localPosition = Vector3.zero; // 구체 중심에 텍스트 배치 textMesh.sortingOrder = 1; // 레이어 순서를 조정하여 구체 위에 배치 .... if (rb == null && collision.collider.CompareTag(P1ballTag)) { TakeDamage(1); textMesh.text = durability.ToString(); // 내구도 변화를 반영 }
추가된건 단 8줄. 이걸로 내구도 표시를 할 수 있다
잘 구현되는것을 확인할 수 있다
<중간 비교>
사실 다 끝난거나 다름없다. 대부분의 기능구현은 끝났고 이제 자잘한 부분들을 자연스럽게 손보면 된다
이쯤에서 중간 비교를 해보자.
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 = FindAnyObjectByType<SPGameManager>(); bGMControl = FindAnyObjectByType<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); } }
이게 기존에 사용하던 BallController
using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; public class ExBallController : MonoBehaviour { SPGameManager spgamemanager; Rigidbody2D rb; bool hasBeenLaunched = false; bool isExpanding = false; // 공이 팽창 중인지 여부 bool isStopped = false; // 공이 완전히 멈췄는지 여부 private float decelerationThreshold = 0.4f; private float dragAmount = 1.1f; private float expandSpeed = 1f; // 팽창 속도 private Vector3 initialScale; // 초기 공 크기 private Vector3 targetScale; // 목표 크기 private int durability; // 공의 내구도 private const string P1ballTag = "P1ball"; // P1ball 태그 캐싱 public PhysicsMaterial2D bouncyMaterial; // 공의 Collider2D에 적용할 물리 재질 private TextMeshPro textMesh; void Start() { spgamemanager = FindAnyObjectByType<SPGameManager>(); rb = GetComponent<Rigidbody2D>(); GameObject textObject = new GameObject("TextMeshPro"); textObject.transform.parent = transform; // 구체의 자식으로 설정 durability = Random.Range(1, 6); textMesh = textObject.AddComponent<TextMeshPro>(); textMesh.text = durability.ToString(); textMesh.fontSize = 4; textMesh.alignment = TextAlignmentOptions.Center; textMesh.autoSizeTextContainer = true; textMesh.rectTransform.localPosition = Vector3.zero; // 구체 중심에 텍스트 배치 textMesh.sortingOrder = 1; // 레이어 순서를 조정하여 구체 위에 배치 rb.drag = 0f; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; rb.interpolation = RigidbodyInterpolation2D.Interpolate; Collider2D collider = GetComponent<Collider2D>(); if (collider != null && bouncyMaterial != null) { collider.sharedMaterial = bouncyMaterial; } initialScale = transform.localScale; } void Update() { if (!hasBeenLaunched && !spgamemanager.isDragging) { LaunchBall(); } if (hasBeenLaunched && !isStopped) { SlowDownBall(); } if (isExpanding) { ExpandBall(); } } void LaunchBall() { Vector2 launchForce = SPGameManager.shotDirection * SPGameManager.shotDistance; rb.AddForce(launchForce, ForceMode2D.Impulse); rb.drag = dragAmount; hasBeenLaunched = true; } void SlowDownBall() { if (rb == null) return; if (rb.velocity.magnitude <= decelerationThreshold) { rb.velocity = Vector2.zero; isStopped = true; StartExpansion(); } } void StartExpansion() { targetScale = initialScale * 10f; 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 (!collision.collider.isTrigger && isExpanding) { isExpanding = false; // 팽창 중단 transform.localScale = transform.localScale; // 현재 크기에서 멈춤 DestroyRigidbody(); // Rigidbody 제거 } if (rb == null && collision.collider.CompareTag(P1ballTag)) { TakeDamage(1); textMesh.text = durability.ToString(); } } void TakeDamage(int damage) { durability -= damage; if (durability <= 0) { Destroy(gameObject); } } void DestroyRigidbody() { if (rb != null) { Destroy(rb); rb = null; } } }
그리고 이게 새롭게 만든 임시 코드이다
우선 딱봐도 Update와 OnCollisionEnter2D 안에 코드들이 확실하게 줄어든게 보인다
하지만 오히려 코드의 양은 후자가 30줄 정도 더 늘었다. 역할에 맞는 메서드들을 만들어서 그런것이다
개인적으로 가장 큰 변화는 코드로 물리엔진을 구현한게 아닌 유니티 자체 엔진을 사용한것. 이게 가장 큰 것 같다
그럼 한번 직접 영상으로 비교해보도록 하자
아...비교가 될줄 알았는데 캡쳐의 한계로 둘의 프레임이 비슷해보인다ㅠ
하지만 괜찮다 적어도 유니티에서는 정말 확실한 프레임 차이가 느껴진다.
'Galaxy Ball > 5. 최적화' 카테고리의 다른 글
최적화 - 7 (0) 2024.09.25 최적화 - 6 (1) 2024.09.24 최적화 - 4 (BallController 스크립트 제작#1) (1) 2024.09.20 최적화 - 3 (1) 2024.09.19 최적화 - 2 (1) 2024.09.10