앱인토스 개발자센터 로고
Skip to content

Unity WebGL 런타임 성능 최적화 가이드

Unity WebGL 환경에서 런타임 성능을 최적화하는 건, 앱인토스 미니앱에서 부드러운 게임 경험을 만드는 핵심이에요.
이 가이드에서는 프레임률 향상, CPU 사용량 감소, GPU 최적화 방법을 함께 다뤄요.


CPU 최적화

1. 프레임률 관리 시스템

c#
using UnityEngine;
using System.Collections;

public class PerformanceManager : MonoBehaviour
{
    [Header("성능 설정")]
    public int targetFrameRate = 60;
    public bool adaptiveFrameRate = true;
    public float performanceCheckInterval = 2f;
    
    [Header("성능 임계값")]
    public float lowPerformanceThreshold = 40f;
    public float highPerformanceThreshold = 55f;
    
    private float averageFrameRate;
    private int qualityLevel;
    private bool isLowPerformanceMode = false;
    
    private void Start()
    {
        qualityLevel = QualitySettings.GetQualityLevel();
        Application.targetFrameRate = targetFrameRate;
        
        if (adaptiveFrameRate)
        {
            StartCoroutine(AdaptivePerformanceRoutine());
        }
    }
    
    private IEnumerator AdaptivePerformanceRoutine()
    {
        while (true)
        {
            yield return new WaitForSeconds(performanceCheckInterval);
            
            averageFrameRate = 1.0f / Time.smoothDeltaTime;
            AdjustPerformanceSettings();
            
            // AppsInToss 플랫폼에 성능 정보 전송
            SendPerformanceDataToAppsInToss();
        }
    }
    
    private void AdjustPerformanceSettings()
    {
        if (averageFrameRate < lowPerformanceThreshold && !isLowPerformanceMode)
        {
            EnableLowPerformanceMode();
        }
        else if (averageFrameRate > highPerformanceThreshold && isLowPerformanceMode)
        {
            DisableLowPerformanceMode();
        }
    }
    
    private void EnableLowPerformanceMode()
    {
        isLowPerformanceMode = true;
        
        // 품질 설정 낮추기
        QualitySettings.SetQualityLevel(Mathf.Max(0, qualityLevel - 1));
        
        // 그림자 비활성화
        QualitySettings.shadows = ShadowQuality.Disable;
        
        // 파티클 수 감소
        QualitySettings.particleRaycastBudget = 16;
        
        // 물리 업데이트 빈도 감소
        Time.fixedDeltaTime = 1f / 30f;
        
        Debug.Log("저성능 모드 활성화됨");
    }
    
    private void DisableLowPerformanceMode()
    {
        isLowPerformanceMode = false;
        
        // 원래 품질 설정 복원
        QualitySettings.SetQualityLevel(qualityLevel);
        QualitySettings.shadows = ShadowQuality.All;
        QualitySettings.particleRaycastBudget = 256;
        Time.fixedDeltaTime = 1f / 50f;
        
        Debug.Log("일반 성능 모드 복원됨");
    }
    
    private void SendPerformanceDataToAppsInToss()
    {
        string performanceData = JsonUtility.ToJson(new {
            frameRate = averageFrameRate,
            isLowPerformanceMode = isLowPerformanceMode,
            qualityLevel = QualitySettings.GetQualityLevel()
        });
        
        Application.ExternalCall("SendPerformanceDataToAppsInToss", performanceData);
    }
}

2. 효율적인 업데이트 관리

c#
using UnityEngine;
using System.Collections.Generic;

public class UpdateManager : MonoBehaviour
{
    private static UpdateManager instance;
    public static UpdateManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<UpdateManager>();
                if (instance == null)
                {
                    GameObject go = new GameObject("UpdateManager");
                    instance = go.AddComponent<UpdateManager>();
                    DontDestroyOnLoad(go);
                }
            }
            return instance;
        }
    }
    
    private List<IUpdatable> updatables = new List<IUpdatable>();
    private List<IFixedUpdatable> fixedUpdatables = new List<IFixedUpdatable>();
    private List<ILateUpdatable> lateUpdatables = new List<ILateUpdatable>();
    
    // 성능 최적화를 위한 업데이트 빈도 제어
    private int frameCount = 0;
    
    private void Update()
    {
        frameCount++;
        
        // 매 프레임 업데이트
        for (int i = updatables.Count - 1; i >= 0; i--)
        {
            if (updatables[i] != null)
                updatables[i].OnUpdate();
        }
        
        // 2프레임마다 업데이트 (30Hz)
        if (frameCount % 2 == 0)
        {
            for (int i = updatables.Count - 1; i >= 0; i--)
            {
                if (updatables[i] is ISlowUpdatable slowUpdatable)
                    slowUpdatable.OnSlowUpdate();
            }
        }
    }
    
    private void FixedUpdate()
    {
        for (int i = fixedUpdatables.Count - 1; i >= 0; i--)
        {
            if (fixedUpdatables[i] != null)
                fixedUpdatables[i].OnFixedUpdate();
        }
    }
    
    private void LateUpdate()
    {
        for (int i = lateUpdatables.Count - 1; i >= 0; i--)
        {
            if (lateUpdatables[i] != null)
                lateUpdatables[i].OnLateUpdate();
        }
    }
    
    public void RegisterUpdatable(IUpdatable updatable)
    {
        if (!updatables.Contains(updatable))
            updatables.Add(updatable);
    }
    
    public void UnregisterUpdatable(IUpdatable updatable)
    {
        updatables.Remove(updatable);
    }
}

// 인터페이스들
public interface IUpdatable
{
    void OnUpdate();
}

public interface ISlowUpdatable
{
    void OnSlowUpdate();
}

public interface IFixedUpdatable
{
    void OnFixedUpdate();
}

public interface ILateUpdatable
{
    void OnLateUpdate();
}

// 사용 예제
public class OptimizedBehavior : MonoBehaviour, IUpdatable, ISlowUpdatable
{
    private Vector3 targetPosition;
    private bool needsPositionUpdate = true;
    
    private void OnEnable()
    {
        UpdateManager.Instance.RegisterUpdatable(this);
    }
    
    private void OnDisable()
    {
        UpdateManager.Instance.UnregisterUpdatable(this);
    }
    
    public void OnUpdate()
    {
        // 매 프레임 필요한 업데이트
        if (needsPositionUpdate)
        {
            transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * 5f);
            
            if (Vector3.Distance(transform.position, targetPosition) < 0.01f)
            {
                needsPositionUpdate = false;
            }
        }
    }
    
    public void OnSlowUpdate()
    {
        // 2프레임마다 수행되는 업데이트 (AI, 경로 찾기 등)
        UpdateAI();
    }
    
    private void UpdateAI()
    {
        // 비용이 큰 AI 로직
    }
}

GPU 최적화

1. 배칭 최적화

c#
using UnityEngine;
using System.Collections.Generic;

public class BatchingOptimizer : MonoBehaviour
{
    [Header("배칭 설정")]
    public bool enableStaticBatching = true;
    public bool enableDynamicBatching = true;
    public bool enableInstancing = true;
    
    [Header("인스턴싱")]
    public GameObject instancedPrefab;
    public Material instancedMaterial;
    public int maxInstances = 1000;
    
    private Matrix4x4[] instanceMatrices;
    private MaterialPropertyBlock propertyBlock;
    private Mesh instanceMesh;
    private List<Vector3> instancePositions = new List<Vector3>();
    
    private void Start()
    {
        SetupBatching();
        SetupInstancing();
    }
    
    private void SetupBatching()
    {
        // 정적 배칭 활성화
        if (enableStaticBatching)
        {
            GameObject[] staticObjects = GameObject.FindGameObjectsWithTag("StaticBatchable");
            StaticBatchingUtility.Combine(staticObjects, this.gameObject);
        }
    }
    
    private void SetupInstancing()
    {
        if (!enableInstancing || instancedPrefab == null) return;
        
        instanceMatrices = new Matrix4x4[maxInstances];
        propertyBlock = new MaterialPropertyBlock();
        
        MeshRenderer renderer = instancedPrefab.GetComponent<MeshRenderer>();
        if (renderer != null)
        {
            instanceMesh = instancedPrefab.GetComponent<MeshFilter>().sharedMesh;
        }
    }
    
    private void Update()
    {
        if (enableInstancing && instanceMesh != null)
        {
            RenderInstances();
        }
    }
    
    private void RenderInstances()
    {
        int instanceCount = Mathf.Min(instancePositions.Count, maxInstances);
        
        for (int i = 0; i < instanceCount; i++)
        {
            instanceMatrices[i] = Matrix4x4.TRS(instancePositions[i], Quaternion.identity, Vector3.one);
        }
        
        Graphics.DrawMeshInstanced(
            instanceMesh, 
            0, 
            instancedMaterial, 
            instanceMatrices, 
            instanceCount, 
            propertyBlock
        );
    }
    
    public void AddInstance(Vector3 position)
    {
        if (instancePositions.Count < maxInstances)
        {
            instancePositions.Add(position);
        }
    }
    
    public void RemoveInstance(Vector3 position)
    {
        instancePositions.Remove(position);
    }
    
    public void ClearInstances()
    {
        instancePositions.Clear();
    }
}

2. 텍스처 아틀라스 관리

c#
using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "TextureAtlasConfig", menuName = "Performance/Texture Atlas Config")]
public class TextureAtlasConfig : ScriptableObject
{
    [System.Serializable]
    public class AtlasEntry
    {
        public string name;
        public Texture2D texture;
        public Rect uvRect;
    }
    
    [Header("아틀라스 설정")]
    public Texture2D atlasTexture;
    public List<AtlasEntry> entries = new List<AtlasEntry>();
    public int atlasSize = 2048;
    
    private Dictionary<string, Rect> uvLookup;
    
    public void Initialize()
    {
        uvLookup = new Dictionary<string, Rect>();
        foreach (var entry in entries)
        {
            uvLookup[entry.name] = entry.uvRect;
        }
    }
    
    public Rect GetUVRect(string textureName)
    {
        if (uvLookup == null) Initialize();
        
        return uvLookup.TryGetValue(textureName, out Rect rect) ? rect : new Rect(0, 0, 1, 1);
    }
}

public class TextureAtlasManager : MonoBehaviour
{
    [Header("아틀라스 설정")]
    public TextureAtlasConfig atlasConfig;
    public Material atlasMaterial;
    
    private void Start()
    {
        if (atlasConfig != null)
        {
            atlasConfig.Initialize();
            ApplyAtlasToMaterials();
        }
    }
    
    private void ApplyAtlasToMaterials()
    {
        // 씬의 모든 렌더러에 아틀라스 적용
        MeshRenderer[] renderers = FindObjectsOfType<MeshRenderer>();
        
        foreach (var renderer in renderers)
        {
            if (renderer.material.name.Contains("Atlasable"))
            {
                renderer.material = atlasMaterial;
                
                // UV 좌표 조정
                string textureName = renderer.name; // 또는 다른 식별 방법
                Rect uvRect = atlasConfig.GetUVRect(textureName);
                renderer.material.SetVector("_MainTex_ST", new Vector4(uvRect.width, uvRect.height, uvRect.x, uvRect.y));
            }
        }
    }
}

AppsInToss 플랫폼 연동

1. 성능 모니터링 시스템

tsx

interface PerformanceMetrics {
    frameRate: number;
    drawCalls: number;
    triangles: number;
    vertices: number;
    memoryUsage: number;
    loadTime: number;
}

class PerformanceMonitor {
    private static instance: PerformanceMonitor;
    private metrics: PerformanceMetrics = {
        frameRate: 0,
        drawCalls: 0,
        triangles: 0,
        vertices: 0,
        memoryUsage: 0,
        loadTime: 0
    };
    
    private frameCount = 0;
    private lastTime = 0;
    
    public static getInstance(): PerformanceMonitor {
        if (!PerformanceMonitor.instance) {
            PerformanceMonitor.instance = new PerformanceMonitor();
        }
        return PerformanceMonitor.instance;
    }
    
    public startMonitoring(): void {
        this.lastTime = performance.now();
        this.monitoringLoop();
    }
    
    private monitoringLoop(): void {
        const currentTime = performance.now();
        this.frameCount++;
        
        // FPS 계산 (1초마다)
        if (currentTime - this.lastTime >= 1000) {
            this.metrics.frameRate = this.frameCount;
            this.frameCount = 0;
            this.lastTime = currentTime;
            
            this.collectMetrics();
        }
        
        requestAnimationFrame(() => this.monitoringLoop());
    }
    
    private collectMetrics(): void {
        // Unity에서 성능 메트릭 수집
        const unityInstance = (window as any).unityInstance;
        if (unityInstance) {
            // Unity에서 렌더링 통계 요청
            unityInstance.SendMessage('PerformanceManager', 'CollectRenderingStats', '');
        }
        
        // 메모리 사용량 수집
        const memInfo = (performance as any).memory;
        if (memInfo) {
            this.metrics.memoryUsage = memInfo.usedJSHeapSize;
        }
    }
    
    public updateMetrics(data: Partial<PerformanceMetrics>): void {
        Object.assign(this.metrics, data);
    }
    
    public getMetrics(): PerformanceMetrics {
        return { ...this.metrics };
    }
    
    // 성능 경고 시스템
    public checkPerformanceWarnings(): void {
        const warnings = [];
        
        if (this.metrics.frameRate < 30) {
            warnings.push('낮은 프레임률 감지');
        }
        
        if (this.metrics.drawCalls > 200) {
            warnings.push('과도한 드로우 콜');
        }
        
        if (this.metrics.memoryUsage > 100 * 1024 * 1024) { // 100MB
            warnings.push('높은 메모리 사용량');
        }
        
    }
}

// Unity에서 호출할 함수들
(window as any).UpdatePerformanceMetrics = (data: string) => {
    const metrics = JSON.parse(data);
    PerformanceMonitor.getInstance().updateMetrics(metrics);
};

(window as any).OnUnityLoaded = () => {
    PerformanceMonitor.getInstance().startMonitoring();
};

2. 동적 품질 조정

c#
using UnityEngine;
using System.Runtime.InteropServices;

public class DynamicQualityManager : MonoBehaviour
{
    [Header("품질 설정")]
    public QualityProfile[] qualityProfiles;
    
    [System.Serializable]
    public class QualityProfile
    {
        public string name;
        public int textureQuality;
        public ShadowQuality shadowQuality;
        public int particleBudget;
        public float renderScale;
        public bool enablePostProcessing;
    }
    
    [DllImport("__Internal")]
    private static extern void SendQualityChangeToAppsInToss(string qualityData);
    
    private int currentQualityIndex = 1; // 보통 품질로 시작
    private float lastFrameTime;
    private int lowFrameCount = 0;
    
    private void Start()
    {
        if (qualityProfiles.Length > 0)
        {
            ApplyQualityProfile(currentQualityIndex);
        }
    }
    
    private void Update()
    {
        MonitorPerformance();
    }
    
    private void MonitorPerformance()
    {
        float currentFrameTime = Time.unscaledDeltaTime;
        
        // 프레임 시간이 33ms(30fps) 이상인 경우
        if (currentFrameTime > 1f / 30f)
        {
            lowFrameCount++;
        }
        else
        {
            lowFrameCount = Mathf.Max(0, lowFrameCount - 1);
        }
        
        // 연속으로 저성능이 감지되면 품질 낮추기
        if (lowFrameCount > 10 && currentQualityIndex > 0)
        {
            ChangeQuality(currentQualityIndex - 1);
            lowFrameCount = 0;
        }
        // 성능이 개선되면 품질 올리기
        else if (lowFrameCount == 0 && currentFrameTime < 1f / 50f && currentQualityIndex < qualityProfiles.Length - 1)
        {
            ChangeQuality(currentQualityIndex + 1);
        }
        
        lastFrameTime = currentFrameTime;
    }
    
    public void ChangeQuality(int qualityIndex)
    {
        if (qualityIndex < 0 || qualityIndex >= qualityProfiles.Length) return;
        
        currentQualityIndex = qualityIndex;
        ApplyQualityProfile(qualityIndex);
        
        // AppsInToss 플랫폼에 품질 변경 알림
        NotifyQualityChange();
    }
    
    private void ApplyQualityProfile(int index)
    {
        QualityProfile profile = qualityProfiles[index];
        
        // 텍스처 품질
        QualitySettings.masterTextureLimit = profile.textureQuality;
        
        // 그림자 품질
        QualitySettings.shadows = profile.shadowQuality;
        
        // 파티클 예산
        QualitySettings.particleRaycastBudget = profile.particleBudget;
        
        // 렌더 스케일 (가능한 경우)
        if (profile.renderScale != 1f)
        {
            Camera.main.pixelRect = new Rect(0, 0, 
                Screen.width * profile.renderScale, 
                Screen.height * profile.renderScale);
        }
        
        // 포스트 프로세싱
        var postProcessVolume = FindObjectOfType<UnityEngine.Rendering.PostProcessing.PostProcessVolume>();
        if (postProcessVolume != null)
        {
            postProcessVolume.enabled = profile.enablePostProcessing;
        }
        
        Debug.Log($"품질 프로필 적용됨: {profile.name}");
    }
    
    private void NotifyQualityChange()
    {
        QualityProfile currentProfile = qualityProfiles[currentQualityIndex];
        
        string qualityData = JsonUtility.ToJson(new {
            qualityLevel = currentQualityIndex,
            qualityName = currentProfile.name,
            timestamp = Time.time
        });
        
        SendQualityChangeToAppsInToss(qualityData);
    }
    
    // 외부에서 품질 강제 변경 (AppsInToss에서 호출)
    public void ForceQualityChange(string qualityData)
    {
        var data = JsonUtility.FromJson<QualityChangeData>(qualityData);
        ChangeQuality(data.qualityLevel);
    }
    
    [System.Serializable]
    private class QualityChangeData
    {
        public int qualityLevel;
    }
}

렌더링 최적화

1. LOD (Level of Detail) 시스템

c#
using UnityEngine;

public class DynamicLODManager : MonoBehaviour
{
    [Header("LOD 설정")]
    public float[] lodDistances = { 50f, 100f, 200f };
    public float cullDistance = 300f;
    public Transform playerTransform;
    
    [Header("성능 기반 LOD")]
    public bool enablePerformanceLOD = true;
    public float performanceThreshold = 45f; // FPS
    
    private LODGroup[] allLODGroups;
    private Camera mainCamera;
    private float performanceLODBias = 1f;
    
    private void Start()
    {
        mainCamera = Camera.main;
        allLODGroups = FindObjectsOfType<LODGroup>();
        
        if (enablePerformanceLOD)
        {
            InvokeRepeating(nameof(UpdatePerformanceLOD), 1f, 2f);
        }
    }
    
    private void Update()
    {
        UpdateLODs();
    }
    
    private void UpdateLODs()
    {
        if (playerTransform == null) return;
        
        foreach (LODGroup lodGroup in allLODGroups)
        {
            if (lodGroup == null) continue;
            
            float distance = Vector3.Distance(playerTransform.position, lodGroup.transform.position);
            
            // 컬링 거리 확인
            if (distance > cullDistance)
            {
                lodGroup.gameObject.SetActive(false);
                continue;
            }
            else if (!lodGroup.gameObject.activeInHierarchy)
            {
                lodGroup.gameObject.SetActive(true);
            }
            
            // 성능 기반 LOD 바이어스 적용
            float adjustedDistance = distance * performanceLODBias;
            
            // LOD 레벨 결정
            int lodLevel = GetLODLevel(adjustedDistance);
            lodGroup.ForceLOD(lodLevel);
        }
    }
    
    private int GetLODLevel(float distance)
    {
        for (int i = 0; i < lodDistances.Length; i++)
        {
            if (distance < lodDistances[i])
                return i;
        }
        return lodDistances.Length; // 가장 낮은 품질 또는 컬링
    }
    
    private void UpdatePerformanceLOD()
    {
        float currentFPS = 1.0f / Time.smoothDeltaTime;
        
        if (currentFPS < performanceThreshold)
        {
            // 성능이 낮으면 LOD 바이어스를 줄여서 더 낮은 품질 사용
            performanceLODBias = Mathf.Max(0.5f, performanceLODBias - 0.1f);
        }
        else if (currentFPS > performanceThreshold + 10f)
        {
            // 성능이 좋으면 LOD 바이어스를 높여서 더 높은 품질 사용
            performanceLODBias = Mathf.Min(1f, performanceLODBias + 0.1f);
        }
        
        QualitySettings.lodBias = performanceLODBias;
    }
}

2. 오클루전 컬링

c#
using UnityEngine;
using System.Collections.Generic;

public class CustomOcclusionCulling : MonoBehaviour
{
    [Header("오클루전 설정")]
    public LayerMask occluderLayers = -1;
    public float raycastDistance = 100f;
    public int raysPerObject = 9; // 3x3 그리드
    
    [Header("성능 설정")]
    public int objectsPerFrame = 10;
    public float updateInterval = 0.1f;
    
    private List<Renderer> allRenderers = new List<Renderer>();
    private Queue<Renderer> renderersToCheck = new Queue<Renderer>();
    private Camera mainCamera;
    
    private void Start()
    {
        mainCamera = Camera.main;
        CollectRenderers();
        InvokeRepeating(nameof(ProcessOcclusion), 0f, updateInterval);
    }
    
    private void CollectRenderers()
    {
        Renderer[] renderers = FindObjectsOfType<Renderer>();
        
        foreach (Renderer renderer in renderers)
        {
            // 스킨드 메시나 파티클은 제외
            if (renderer is MeshRenderer || renderer is SpriteRenderer)
            {
                allRenderers.Add(renderer);
                renderersToCheck.Enqueue(renderer);
            }
        }
    }
    
    private void ProcessOcclusion()
    {
        int processedCount = 0;
        
        while (renderersToCheck.Count > 0 && processedCount < objectsPerFrame)
        {
            Renderer renderer = renderersToCheck.Dequeue();
            
            if (renderer != null && renderer.gameObject.activeInHierarchy)
            {
                bool isVisible = CheckVisibility(renderer);
                renderer.enabled = isVisible;
                
                // 다음 프레임에 다시 검사하도록 큐에 추가
                renderersToCheck.Enqueue(renderer);
            }
            
            processedCount++;
        }
    }
    
    private bool CheckVisibility(Renderer renderer)
    {
        Bounds bounds = renderer.bounds;
        Vector3 cameraPosition = mainCamera.transform.position;
        
        // 카메라 프러스텀 내부에 있는지 확인
        if (!GeometryUtility.TestPlanesAABB(GeometryUtility.CalculateFrustumPlanes(mainCamera), bounds))
        {
            return false;
        }
        
        // 레이캐스트를 통한 오클루전 테스트
        Vector3 boundsCenter = bounds.center;
        Vector3 boundsSize = bounds.size;
        
        int visibleRays = 0;
        
        // 3x3 그리드로 레이 발사
        for (int x = -1; x <= 1; x++)
        {
            for (int y = -1; y <= 1; y++)
            {
                Vector3 testPoint = boundsCenter + new Vector3(
                    x * boundsSize.x * 0.25f,
                    y * boundsSize.y * 0.25f,
                    0
                );
                
                Vector3 direction = (testPoint - cameraPosition).normalized;
                float distance = Vector3.Distance(cameraPosition, testPoint);
                
                if (!Physics.Raycast(cameraPosition, direction, distance, occluderLayers))
                {
                    visibleRays++;
                    if (visibleRays > raysPerObject / 3) // 1/3 이상 보이면 렌더링
                    {
                        return true;
                    }
                }
            }
        }
        
        return visibleRays > 0;
    }
}

물리 최적화

1. 물리 업데이트 관리

c#
using UnityEngine;
using System.Collections.Generic;

public class PhysicsOptimizer : MonoBehaviour
{
    [Header("물리 설정")]
    public float physicsTimeStep = 0.02f; // 50Hz
    public int maxSubSteps = 8;
    public float sleepThreshold = 0.005f;
    
    [Header("성능 기반 조정")]
    public bool enableAdaptivePhysics = true;
    public float targetFrameRate = 60f;
    
    private List<Rigidbody> managedRigidbodies = new List<Rigidbody>();
    private float originalFixedDeltaTime;
    
    private void Start()
    {
        originalFixedDeltaTime = Time.fixedDeltaTime;
        Time.fixedDeltaTime = physicsTimeStep;
        
        Physics.sleepThreshold = sleepThreshold;
        
        CollectRigidbodies();
        
        if (enableAdaptivePhysics)
        {
            InvokeRepeating(nameof(AdaptPhysicsSettings), 1f, 2f);
        }
    }
    
    private void CollectRigidbodies()
    {
        Rigidbody[] rigidbodies = FindObjectsOfType<Rigidbody>();
        
        foreach (Rigidbody rb in rigidbodies)
        {
            managedRigidbodies.Add(rb);
            OptimizeRigidbody(rb);
        }
    }
    
    private void OptimizeRigidbody(Rigidbody rb)
    {
        // 잠자기 설정 최적화
        rb.sleepThreshold = sleepThreshold;
        
        // 불필요한 물리 연산 비활성화
        if (rb.GetComponent<Collider>() == null)
        {
            rb.detectCollisions = false;
        }
        
        // 정적 오브젝트는 키네마틱으로 설정
        if (rb.GetComponent<Collider>() != null && rb.GetComponent<Collider>().isTrigger)
        {
            rb.isKinematic = true;
        }
    }
    
    private void AdaptPhysicsSettings()
    {
        float currentFrameRate = 1.0f / Time.smoothDeltaTime;
        
        if (currentFrameRate < targetFrameRate - 10f)
        {
            // 성능이 낮으면 물리 업데이트 빈도 감소
            Time.fixedDeltaTime = Mathf.Min(0.033f, Time.fixedDeltaTime + 0.002f);
            
            // 일부 리지드바디를 잠자기 상태로 전환
            SleepDistantRigidbodies();
        }
        else if (currentFrameRate > targetFrameRate + 5f)
        {
            // 성능이 좋으면 물리 업데이트 빈도 증가
            Time.fixedDeltaTime = Mathf.Max(physicsTimeStep, Time.fixedDeltaTime - 0.002f);
        }
    }
    
    private void SleepDistantRigidbodies()
    {
        Vector3 playerPosition = Camera.main.transform.position;
        float maxActiveDistance = 50f;
        
        foreach (Rigidbody rb in managedRigidbodies)
        {
            if (rb == null) continue;
            
            float distance = Vector3.Distance(rb.transform.position, playerPosition);
            
            if (distance > maxActiveDistance && !rb.IsSleeping())
            {
                rb.Sleep();
            }
            else if (distance <= maxActiveDistance && rb.IsSleeping())
            {
                rb.WakeUp();
            }
        }
    }
}

프로파일링 및 디버깅

1. 성능 분석 도구

c#
using UnityEngine;
using System.Text;
using System.Collections.Generic;

public class PerformanceProfiler : MonoBehaviour
{
    [Header("프로파일링 설정")]
    public bool enableProfiling = true;
    public KeyCode toggleKey = KeyCode.F1;
    public float updateInterval = 1f;
    
    private Dictionary<string, float> performanceData = new Dictionary<string, float>();
    private StringBuilder displayText = new StringBuilder();
    private bool showGUI = false;
    
    private void Update()
    {
        if (Input.GetKeyDown(toggleKey))
        {
            showGUI = !showGUI;
        }
        
        if (enableProfiling)
        {
            CollectPerformanceData();
        }
    }
    
    private void CollectPerformanceData()
    {
        // FPS
        performanceData["FPS"] = 1.0f / Time.smoothDeltaTime;
        
        // 메모리 사용량
        performanceData["Memory (MB)"] = System.GC.GetTotalMemory(false) / 1024f / 1024f;
        
        // 렌더링 통계
        performanceData["SetPass Calls"] = UnityEngine.Rendering.FrameDebugger.enabled ? 
            UnityEngine.Rendering.FrameDebugger.GetFrameEventCount() : 0;
            
        // 활성 오브젝트 수
        performanceData["Active GameObjects"] = FindObjectsOfType<GameObject>().Length;
        
        // 활성 렌더러 수
        performanceData["Active Renderers"] = FindObjectsOfType<Renderer>().Length;
    }
    
    private void OnGUI()
    {
        if (!showGUI) return;
        
        displayText.Clear();
        displayText.AppendLine("=== 성능 분석 ===");
        
        foreach (var kvp in performanceData)
        {
            displayText.AppendFormat("{0}: {1:F2}\n", kvp.Key, kvp.Value);
        }
        
        GUI.Box(new Rect(10, 10, 300, 200), displayText.ToString());
    }
    
    public void LogPerformanceData()
    {
        StringBuilder log = new StringBuilder();
        log.AppendLine("Performance Report:");
        
        foreach (var kvp in performanceData)
        {
            log.AppendFormat("{0}: {1:F2}\n", kvp.Key, kvp.Value);
        }
        
        Debug.Log(log.ToString());
        
        // AppsInToss 플랫폼에 전송
        Application.ExternalCall("SendPerformanceReport", log.ToString());
    }
}

베스트 프랙티스

  • 적응형 성능 관리 - 실시간으로 성능을 모니터링하고 품질을 조정
  • 효율적인 업데이트 - Update 함수 대신 이벤트 기반 시스템 사용
  • 배칭 최적화 - 드로우 콜 수 최소화
  • 메모리 관리 - 불필요한 할당과 가비지 생성 방지
  • AppsInToss 플랫폼 활용 - 네이티브 성능 모니터링 기능 활용