Otimização de Performance em Jogos: Como Garantir 60 FPS Estáveis

Guia completo de otimização de performance: profiling, técnicas avançadas, GPU/CPU optimization e estratégias para alcançar 60 FPS
2024-03-20
Índice do Conteúdo
Artigos Relacionados
Otimização de Performance em Jogos: Como Garantir 60 FPS Estáveis
Introdução: A Importância dos 60 FPS
Performance é fundamental para a experiência do jogador. A diferença entre 30 e 60 FPS pode determinar o sucesso ou fracasso de um jogo, especialmente em gêneros competitivos. Este guia abrangente explorará técnicas avançadas de otimização, ferramentas de profiling e estratégias para alcançar e manter 60 FPS estáveis em diferentes plataformas.
Por Que 60 FPS é o Padrão?
60 FPS oferece fluidez visual, menor input lag e melhor responsividade. Para jogos competitivos, é essencial. Para experiências imersivas, é transformador. Dominar a otimização para alcançar este objetivo consistentemente é uma habilidade crucial para todo desenvolvedor.
Profiling: Identificando Gargalos
Ferramentas de Profiling
using UnityEngine;
using UnityEngine.Profiling;
using System.Diagnostics;
public class PerformanceProfiler : MonoBehaviour
{
private float deltaTime = 0.0f;
private Recorder drawCallRecorder;
private Recorder setPassRecorder;
private Recorder triangleRecorder;
private Recorder vertexRecorder;
void Start()
{
// Inicializar recorders do Unity Profiler
drawCallRecorder = Recorder.Get("Draw Calls Count");
setPassRecorder = Recorder.Get("SetPass Calls Count");
triangleRecorder = Recorder.Get("Triangles Count");
vertexRecorder = Recorder.Get("Vertices Count");
drawCallRecorder.enabled = true;
setPassRecorder.enabled = true;
triangleRecorder.enabled = true;
vertexRecorder.enabled = true;
}
void Update()
{
// Calcular FPS
deltaTime += (Time.unscaledDeltaTime - deltaTime) * 0.1f;
// Custom profiling markers
using (new ProfilerMarker("MySystem.Update").Auto())
{
UpdateGameSystems();
}
}
void OnGUI()
{
if (Application.isEditor || UnityEngine.Debug.isDebugBuild)
{
DisplayPerformanceStats();
}
}
void DisplayPerformanceStats()
{
int w = Screen.width, h = Screen.height;
GUIStyle style = new GUIStyle();
Rect rect = new Rect(10, 10, w, h * 2 / 100);
style.alignment = TextAnchor.UpperLeft;
style.fontSize = h * 2 / 75;
style.normal.textColor = Color.white;
float msec = deltaTime * 1000.0f;
float fps = 1.0f / deltaTime;
string text = string.Format(
"FPS: {0:0.} ({1:0.0} ms)\n" +
"Draw Calls: {2}\n" +
"SetPass Calls: {3}\n" +
"Triangles: {4:0,0}\n" +
"Vertices: {5:0,0}\n" +
"Memory: {6:0} MB",
fps, msec,
drawCallRecorder.sampleBlockCount,
setPassRecorder.sampleBlockCount,
triangleRecorder.sampleBlockCount,
vertexRecorder.sampleBlockCount,
GC.GetTotalMemory(false) / 1048576
);
GUI.Label(rect, text, style);
}
}
Análise de Frame Budget
# Sistema de análise de frame budget
class FrameBudgetAnalyzer:
def __init__(self, target_fps=60):
self.target_fps = target_fps
self.frame_time_ms = 1000 / target_fps # 16.67ms para 60 FPS
def analyze_frame_breakdown(self):
"""Breakdown típico de um frame a 60 FPS"""
budget = {
"cpu": {
"gameplay_logic": 3.0, # ms
"physics": 2.5,
"ai": 2.0,
"animation": 1.5,
"scripting": 1.0,
"audio": 0.5,
"input": 0.3,
"other": 1.2,
"total": 12.0
},
"gpu": {
"geometry": 3.0,
"shadows": 2.5,
"lighting": 2.0,
"post_processing": 2.0,
"particles": 1.5,
"ui": 1.0,
"total": 12.0
},
"vsync_overhead": 1.0,
"system": 3.67
}
total = sum([
budget["cpu"]["total"],
budget["gpu"]["total"],
budget["vsync_overhead"],
budget["system"]
])
return {
"budget": budget,
"total_ms": total,
"within_budget": total <= self.frame_time_ms,
"headroom": self.frame_time_ms - total
}
def identify_bottleneck(self, frame_data):
"""Identificar se é CPU ou GPU bound"""
if frame_data["gpu_time"] > frame_data["cpu_time"]:
return {
"bottleneck": "GPU",
"severity": frame_data["gpu_time"] / self.frame_time_ms,
"recommendations": [
"Reduzir qualidade de sombras",
"Diminuir resolução de renderização",
"Otimizar shaders",
"Reduzir post-processing"
]
}
else:
return {
"bottleneck": "CPU",
"severity": frame_data["cpu_time"] / self.frame_time_ms,
"recommendations": [
"Otimizar scripts",
"Reduzir física complexa",
"Implementar LOD para AI",
"Use object pooling"
]
}
Otimização de CPU
Gestão Eficiente de GameObjects
public class CPUOptimization : MonoBehaviour
{
// Object Pooling para reduzir GC e instantiation
public class ObjectPool<T> where T : Component
{
private Queue<T> pool = new Queue<T>();
private GameObject prefab;
private Transform parent;
private int initialSize;
public ObjectPool(GameObject prefab, int initialSize, Transform parent = null)
{
this.prefab = prefab;
this.initialSize = initialSize;
this.parent = parent;
// Pre-warm pool
for (int i = 0; i < initialSize; i++)
{
T obj = GameObject.Instantiate(prefab, parent).GetComponent<T>();
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
public T Get()
{
T obj;
if (pool.Count > 0)
{
obj = pool.Dequeue();
}
else
{
obj = GameObject.Instantiate(prefab, parent).GetComponent<T>();
}
obj.gameObject.SetActive(true);
return obj;
}
public void Return(T obj)
{
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
// Spatial Hashing para otimizar detecção de colisão
public class SpatialHashGrid
{
private Dictionary<int, List<GameObject>> grid;
private float cellSize;
public SpatialHashGrid(float cellSize)
{
this.cellSize = cellSize;
this.grid = new Dictionary<int, List<GameObject>>();
}
private int GetCellKey(Vector3 position)
{
int x = Mathf.FloorToInt(position.x / cellSize);
int z = Mathf.FloorToInt(position.z / cellSize);
return x * 73856093 ^ z * 19349663; // Hash function
}
public void Insert(GameObject obj)
{
int key = GetCellKey(obj.transform.position);
if (!grid.ContainsKey(key))
{
grid[key] = new List<GameObject>();
}
grid[key].Add(obj);
}
public List<GameObject> GetNearby(Vector3 position, float radius)
{
List<GameObject> nearby = new List<GameObject>();
int cellRadius = Mathf.CeilToInt(radius / cellSize);
for (int x = -cellRadius; x <= cellRadius; x++)
{
for (int z = -cellRadius; z <= cellRadius; z++)
{
Vector3 cellPos = position + new Vector3(x * cellSize, 0, z * cellSize);
int key = GetCellKey(cellPos);
if (grid.ContainsKey(key))
{
nearby.AddRange(grid[key]);
}
}
}
return nearby;
}
}
}
Otimização de Scripts
// Técnicas de otimização de código JavaScript/Unity
class ScriptOptimization {
constructor() {
// Cache de referências
this.cachedTransform = null
this.cachedRenderer = null
this.cachedRigidbody = null
}
// RUIM: Busca componente toda vez
badUpdate() {
let position = this.gameObject.GetComponent('Transform').position
let renderer = this.gameObject.GetComponent('Renderer')
// ... operações
}
// BOM: Cache de componentes
start() {
this.cachedTransform = this.transform
this.cachedRenderer = this.GetComponent('Renderer')
this.cachedRigidbody = this.GetComponent('Rigidbody')
}
goodUpdate() {
let position = this.cachedTransform.position
// ... operações com componentes cacheados
}
// Otimização de loops
optimizeLoops() {
const array = new Array(10000)
// RUIM: Length calculado toda iteração
for (let i = 0; i < array.length; i++) {
// processo
}
// BOM: Length cacheado
const length = array.length
for (let i = 0; i < length; i++) {
// processo
}
// MELHOR: Loop reverso (quando ordem não importa)
for (let i = array.length - 1; i >= 0; i--) {
// processo
}
}
// Evitar alocações desnecessárias
avoidAllocations() {
// RUIM: Cria novo array toda vez
function badGetNeighbors() {
return [1, 2, 3, 4, 5]
}
// BOM: Reutiliza array
const neighborsCache = new Array(5)
function goodGetNeighbors() {
neighborsCache[0] = 1
neighborsCache[1] = 2
// ... preenche array
return neighborsCache
}
}
}
Threading e Jobs System
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
// Job System para cálculos paralelos
[BurstCompile]
public struct ParallelCalculationJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> positions;
[ReadOnly] public float3 targetPosition;
public NativeArray<float> distances;
public void Execute(int index)
{
distances[index] = math.distance(positions[index], targetPosition);
}
}
public class JobSystemOptimization : MonoBehaviour
{
private NativeArray<float3> positions;
private NativeArray<float> distances;
void Start()
{
int entityCount = 10000;
positions = new NativeArray<float3>(entityCount, Allocator.Persistent);
distances = new NativeArray<float>(entityCount, Allocator.Persistent);
// Inicializar posições
for (int i = 0; i < entityCount; i++)
{
positions[i] = new float3(
UnityEngine.Random.Range(-100f, 100f),
0,
UnityEngine.Random.Range(-100f, 100f)
);
}
}
void Update()
{
// Criar e agendar job
var job = new ParallelCalculationJob
{
positions = positions,
targetPosition = float3.zero,
distances = distances
};
// Executar em paralelo com batch size de 32
JobHandle handle = job.Schedule(positions.Length, 32);
// Fazer outro trabalho enquanto job executa
DoOtherWork();
// Esperar conclusão quando necessário
handle.Complete();
// Usar resultados
ProcessDistances();
}
void OnDestroy()
{
positions.Dispose();
distances.Dispose();
}
}
Otimização de GPU
Batching e Draw Calls
public class GPUOptimization : MonoBehaviour
{
// Static Batching para objetos estáticos
void SetupStaticBatching()
{
// Marcar objetos como static no Inspector
// Unity automaticamente faz batch
// Ou programaticamente
GameObject[] staticObjects = GameObject.FindGameObjectsWithTag("Static");
StaticBatchingUtility.Combine(staticObjects, gameObject);
}
// Dynamic Batching settings
void ConfigureDynamicBatching()
{
// Requisitos para dynamic batching:
// - Menos de 900 vértices
// - Mesmo material
// - Sem lightmaps diferentes
// - Sem multi-pass shaders
// Configurar no Player Settings
// Edit -> Project Settings -> Player -> Other Settings
}
// GPU Instancing para múltiplas cópias
public class GPUInstancingRenderer : MonoBehaviour
{
public Mesh mesh;
public Material material;
private Matrix4x4[] matrices;
private MaterialPropertyBlock propertyBlock;
void Start()
{
// Material deve ter "Enable GPU Instancing" marcado
material.enableInstancing = true;
matrices = new Matrix4x4[1023]; // Máximo por draw call
propertyBlock = new MaterialPropertyBlock();
// Criar matrizes para instâncias
for (int i = 0; i < matrices.Length; i++)
{
Vector3 position = new Vector3(
Random.Range(-50f, 50f),
0,
Random.Range(-50f, 50f)
);
Quaternion rotation = Quaternion.Euler(0, Random.Range(0, 360), 0);
Vector3 scale = Vector3.one;
matrices[i] = Matrix4x4.TRS(position, rotation, scale);
}
}
void Update()
{
// Renderizar todas as instâncias em um draw call
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, matrices.Length, propertyBlock);
}
}
}
Otimização de Shaders
// Shader otimizado para mobile
Shader "Optimized/MobileShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// Otimizações
#pragma multi_compile_instancing
#pragma multi_compile_fog
// Remover features não usadas
#pragma skip_variants SHADOWS_SOFT
#pragma skip_variants DIRLIGHTMAP_COMBINED
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
float4 _MainTex_ST;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o, o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
// Sampling simples
fixed4 col = tex2D(_MainTex, i.uv);
col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
// Apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
// Fallback para hardware mais fraco
FallBack "Mobile/Diffuse"
}
LOD (Level of Detail)
public class LODOptimization : MonoBehaviour
{
public class DynamicLODSystem
{
public GameObject[] lodModels; // Do maior para menor detalhe
public float[] lodDistances = { 10f, 25f, 50f, 100f };
private Camera mainCamera;
private int currentLOD = -1;
void Start()
{
mainCamera = Camera.main;
}
void Update()
{
float distance = Vector3.Distance(
transform.position,
mainCamera.transform.position
);
int newLOD = CalculateLOD(distance);
if (newLOD != currentLOD)
{
SwitchLOD(newLOD);
currentLOD = newLOD;
}
}
int CalculateLOD(float distance)
{
for (int i = 0; i < lodDistances.Length; i++)
{
if (distance < lodDistances[i])
{
return i;
}
}
return lodModels.Length - 1; // Menor LOD
}
void SwitchLOD(int lodLevel)
{
// Desativar todos
foreach (var model in lodModels)
{
model.SetActive(false);
}
// Ativar LOD correto
if (lodLevel < lodModels.Length)
{
lodModels[lodLevel].SetActive(true);
}
}
}
// LOD para terreno
public class TerrainLOD
{
public void OptimizeTerrain(Terrain terrain)
{
// Ajustar pixel error baseado em distância
terrain.heightmapPixelError = 5; // 1-200, menor = mais detalhe
// Reduzir detalhe de vegetação distante
terrain.detailObjectDistance = 80; // Distância máxima para detalhes
// Billboarding para árvores distantes
terrain.treeBillboardDistance = 50;
// Reduzir resolução de sombras para terreno
terrain.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.TwoSided;
}
}
}
Otimização de Memória
Gestão de Texturas e Assets
# Sistema de gestão de memória para assets
class MemoryOptimization:
def __init__(self):
self.texture_budget_mb = 512 # Mobile
self.mesh_budget_mb = 256
self.audio_budget_mb = 128
def calculate_texture_memory(self, width, height, format):
"""Calcular memória usada por textura"""
bytes_per_pixel = {
"RGBA32": 4,
"RGB24": 3,
"RGB565": 2,
"PVRTC4": 0.5,
"ETC2": 0.5,
"ASTC4x4": 1,
"DXT1": 0.5,
"DXT5": 1
}
bpp = bytes_per_pixel.get(format, 4)
size_bytes = width * height * bpp
# Considerar mipmaps (adiciona ~33%)
with_mipmaps = size_bytes * 1.33
return {
"format": format,
"resolution": f"{width}x{height}",
"memory_mb": with_mipmaps / (1024 * 1024),
"with_mipmaps": True
}
def optimize_texture_settings(self, platform):
"""Configurações otimizadas por plataforma"""
settings = {
"mobile": {
"max_resolution": 1024,
"compression": "PVRTC4" if platform == "ios" else "ETC2",
"aniso_level": 2,
"mipmaps": True,
"read_write": False # Economiza 50% memória
},
"pc": {
"max_resolution": 2048,
"compression": "DXT5",
"aniso_level": 8,
"mipmaps": True,
"read_write": False
},
"console": {
"max_resolution": 4096,
"compression": "BC7",
"aniso_level": 16,
"mipmaps": True,
"read_write": False
}
}
return settings.get(platform, settings["pc"])
def audio_optimization(self):
"""Otimização de memória de áudio"""
return {
"music": {
"format": "Vorbis",
"quality": 0.4, # 0-1
"load_type": "Streaming",
"compression": "Compressed"
},
"sfx_short": {
"format": "ADPCM",
"load_type": "Decompress on load",
"compression": "Compressed"
},
"sfx_frequent": {
"format": "PCM",
"load_type": "Load in memory",
"compression": "None" # Para baixa latência
},
"voice": {
"format": "MP3",
"quality": 0.5,
"load_type": "Streaming",
"compression": "Compressed"
}
}
Garbage Collection
public class GCOptimization : MonoBehaviour
{
// Evitar alocações no Update
private Vector3[] positionsCache;
private List<GameObject> visibleObjects;
private StringBuilder stringBuilder;
void Start()
{
// Pre-alocar coleções
positionsCache = new Vector3[100];
visibleObjects = new List<GameObject>(100);
stringBuilder = new StringBuilder(256);
}
// RUIM: Cria lixo toda frame
void BadUpdate()
{
string status = "Health: " + health + " Score: " + score;
Vector3[] positions = new Vector3[enemyCount];
List<GameObject> visible = new List<GameObject>();
}
// BOM: Reutiliza objetos
void GoodUpdate()
{
// Reutilizar StringBuilder
stringBuilder.Clear();
stringBuilder.Append("Health: ");
stringBuilder.Append(health);
stringBuilder.Append(" Score: ");
stringBuilder.Append(score);
// Reutilizar array
for (int i = 0; i < enemyCount && i < positionsCache.Length; i++)
{
positionsCache[i] = GetEnemyPosition(i);
}
// Clear e reutilizar lista
visibleObjects.Clear();
// ... adicionar objetos visíveis
}
// Controle manual de GC
public class GCManager : MonoBehaviour
{
private float gcTimer = 0f;
private const float GC_INTERVAL = 30f; // GC a cada 30 segundos
void Update()
{
gcTimer += Time.deltaTime;
if (gcTimer >= GC_INTERVAL)
{
// Forçar GC em momento controlado (ex: entre níveis)
if (IsGoodTimeForGC())
{
System.GC.Collect();
gcTimer = 0f;
}
}
}
bool IsGoodTimeForGC()
{
// Durante loading, menu, ou momentos não-críticos
return GameState.IsLoading || GameState.IsPaused;
}
}
}
Otimização Multi-plataforma
Adaptive Quality
public class AdaptiveQuality : MonoBehaviour
{
private float targetFrameTime = 16.67f; // 60 FPS
private float[] frameTimes = new float[30];
private int frameIndex = 0;
private int currentQualityLevel;
public class QualityPreset
{
public int shadowQuality;
public float shadowDistance;
public int textureQuality;
public int anisotropicFiltering;
public bool softParticles;
public int antiAliasing;
public float lodBias;
public int maxLODLevel;
public float renderScale;
}
private Dictionary<int, QualityPreset> qualityPresets = new Dictionary<int, QualityPreset>
{
[0] = new QualityPreset // Ultra Low
{
shadowQuality = 0,
shadowDistance = 0,
textureQuality = 2,
anisotropicFiltering = 0,
softParticles = false,
antiAliasing = 0,
lodBias = 0.3f,
maxLODLevel = 3,
renderScale = 0.5f
},
[1] = new QualityPreset // Low
{
shadowQuality = 1,
shadowDistance = 20,
textureQuality = 1,
anisotropicFiltering = 0,
softParticles = false,
antiAliasing = 0,
lodBias = 0.5f,
maxLODLevel = 2,
renderScale = 0.75f
},
[2] = new QualityPreset // Medium
{
shadowQuality = 2,
shadowDistance = 40,
textureQuality = 0,
anisotropicFiltering = 2,
softParticles = true,
antiAliasing = 2,
lodBias = 1.0f,
maxLODLevel = 1,
renderScale = 1.0f
},
[3] = new QualityPreset // High
{
shadowQuality = 3,
shadowDistance = 100,
textureQuality = 0,
anisotropicFiltering = 8,
softParticles = true,
antiAliasing = 4,
lodBias = 1.5f,
maxLODLevel = 0,
renderScale = 1.0f
}
};
void Update()
{
// Registrar frame time
frameTimes[frameIndex] = Time.deltaTime * 1000f;
frameIndex = (frameIndex + 1) % frameTimes.Length;
// Ajustar qualidade a cada segundo
if (frameIndex == 0)
{
AdjustQuality();
}
}
void AdjustQuality()
{
float avgFrameTime = 0f;
foreach (float time in frameTimes)
{
avgFrameTime += time;
}
avgFrameTime /= frameTimes.Length;
// Se está muito abaixo do target, aumentar qualidade
if (avgFrameTime < targetFrameTime * 0.8f && currentQualityLevel < 3)
{
currentQualityLevel++;
ApplyQualityPreset(currentQualityLevel);
}
// Se está acima do target, reduzir qualidade
else if (avgFrameTime > targetFrameTime * 1.1f && currentQualityLevel > 0)
{
currentQualityLevel--;
ApplyQualityPreset(currentQualityLevel);
}
}
void ApplyQualityPreset(int level)
{
var preset = qualityPresets[level];
QualitySettings.shadows = (ShadowQuality)preset.shadowQuality;
QualitySettings.shadowDistance = preset.shadowDistance;
QualitySettings.masterTextureLimit = preset.textureQuality;
QualitySettings.anisotropicFiltering = (AnisotropicFiltering)preset.anisotropicFiltering;
QualitySettings.softParticles = preset.softParticles;
QualitySettings.antiAliasing = preset.antiAliasing;
QualitySettings.lodBias = preset.lodBias;
QualitySettings.maximumLODLevel = preset.maxLODLevel;
// Ajustar render scale
UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset urpAsset =
(UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset)QualitySettings.renderPipeline;
if (urpAsset != null)
{
urpAsset.renderScale = preset.renderScale;
}
UnityEngine.Debug.Log($"Quality adjusted to level {level}");
}
}
Ferramentas e Métricas
Dashboard de Performance
// Sistema de monitoramento de performance em tempo real
class PerformanceDashboard {
constructor() {
this.metrics = {
fps: [],
frameTime: [],
drawCalls: [],
triangles: [],
memoryUsage: [],
gcCollects: [],
}
this.thresholds = {
fps: { min: 55, target: 60, max: 120 },
frameTime: { min: 8.33, target: 16.67, max: 33.33 },
drawCalls: { min: 50, target: 100, max: 300 },
triangles: { min: 50000, target: 100000, max: 500000 },
}
}
collectMetrics() {
const current = {
fps: this.calculateFPS(),
frameTime: performance.now() - this.lastFrame,
drawCalls: this.getDrawCalls(),
triangles: this.getTriangleCount(),
memoryUsage: this.getMemoryUsage(),
gcCollects: this.getGCCount(),
}
// Manter histórico dos últimos 60 frames
for (let metric in this.metrics) {
this.metrics[metric].push(current[metric])
if (this.metrics[metric].length > 60) {
this.metrics[metric].shift()
}
}
return current
}
analyzePerformance() {
const analysis = {
status: 'Good',
issues: [],
recommendations: [],
}
// Analisar FPS
const avgFPS = this.average(this.metrics.fps)
if (avgFPS < this.thresholds.fps.min) {
analysis.status = 'Critical'
analysis.issues.push(`FPS baixo: ${avgFPS.toFixed(1)}`)
analysis.recommendations.push('Reduzir qualidade gráfica')
}
// Analisar Draw Calls
const avgDrawCalls = this.average(this.metrics.drawCalls)
if (avgDrawCalls > this.thresholds.drawCalls.max) {
analysis.issues.push(`Draw calls alto: ${avgDrawCalls}`)
analysis.recommendations.push('Implementar batching')
}
// Detectar spikes
const spikes = this.detectSpikes(this.metrics.frameTime)
if (spikes.length > 0) {
analysis.issues.push(`${spikes.length} frame spikes detectados`)
analysis.recommendations.push('Investigar operações síncronas')
}
return analysis
}
detectSpikes(data, threshold = 2) {
const avg = this.average(data)
return data.filter((value) => value > avg * threshold)
}
average(arr) {
return arr.reduce((a, b) => a + b, 0) / arr.length
}
}
Recursos e Ferramentas
Ferramentas de Profiling
- Unity Profiler: Análise completa built-in
- Frame Debugger: Visualizar draw calls
- RenderDoc: GPU debugging detalhado
- Intel VTune: CPU profiling avançado
- NVIDIA Nsight: GPU profiling NVIDIA
Benchmarking Tools
- Unity Performance Testing: Framework de testes
- Benchmark.NET: Micro-benchmarking
- PerfView: Análise de performance .NET
- Chrome DevTools: Para WebGL games
Conclusão
Alcançar 60 FPS estáveis requer disciplina, ferramentas adequadas e otimização contínua. Não existe bala de prata - é necessário perfilar, identificar gargalos e aplicar técnicas específicas para cada situação. A chave é estabelecer um orçamento de performance desde o início e monitorar constantemente.
⚡ Domine Performance em Games! Aprenda técnicas avançadas de otimização com profissionais da indústria. Teste vocacional gratuito →
Próximos Passos
Comece estabelecendo métricas de performance claras. Implemente profiling desde o início do desenvolvimento. Teste em hardware real, especialmente dispositivos mais fracos. Lembre-se: é mais fácil manter boa performance do que recuperá-la depois.
🚀 Curso de Otimização Avançada! Torne-se especialista em performance de jogos. Inscreva-se agora →
