11. Performance Optimization

Defold Platformer Framework
# 11. Performance Optimization When developing games with the Defold Platformer Framework, optimizing performance is crucial to ensure smooth gameplay across different devices. This section covers techniques and best practices to maximize your game's performance. ## Table of Contents - [Understanding Performance Bottlenecks](#understanding-performance-bottlenecks) - [Optimizing Game Objects](#optimizing-game-objects) - [Sprite and Animation Optimization](#sprite-and-animation-optimization) - [Physics Optimization](#physics-optimization) - [Memory Management](#memory-management) - [Profiling and Benchmarking](#profiling-and-benchmarking) - [Platform-Specific Optimizations](#platform-specific-optimizations) ## Understanding Performance Bottlenecks Performance issues typically stem from one of these areas: 1. **CPU Bottlenecks** - Too much logic processing per frame 2. **GPU Bottlenecks** - Rendering too many or complex visual elements 3. **Memory Bottlenecks** - Inefficient memory usage or leaks 4. **I/O Bottlenecks** - Slow asset loading or file operations Understanding which area is causing performance issues is the first step to effective optimization. ## Optimizing Game Objects ### Object Pooling Instead of creating and destroying objects frequently, implement an object pool: ```lua -- Simple object pool implementation local pool = {} local pool_available = {} function init_pool(count, factory_url) for i = 1, count do local id = factory.create(factory_url) go.set_scale(0.001, id) -- Hide object table.insert(pool, id) table.insert(pool_available, id) end end function get_object_from_pool() if #pool_available > 0 then local id = table.remove(pool_available) go.set_scale(1, id) -- Show object return id end return nil -- Pool empty end function return_to_pool(id) go.set_scale(0.001, id) -- Hide object table.insert(pool_available, id) end ``` ### Collection Proxies Use collection proxies to load and unload entire game sections: ```lua -- In main.script function init() msg.post("#level1_proxy", "load") end function on_message(self, message_id, message, sender) if message_id == hash("proxy_loaded") then msg.post(sender, "enable") elseif message_id == hash("load_level2") then msg.post("#level1_proxy", "unload") msg.post("#level2_proxy", "load") end end ``` ### Disable Unused Components Disable components when they're not needed: ```lua -- Disable physics for off-screen objects function disable_physics(id) msg.post(id, "disable") -- Re-enable when needed -- msg.post(id, "enable") end ``` ## Sprite and Animation Optimization ### Atlas Optimization Group related sprites into atlases to reduce draw calls: ``` // atlas.atlas images { image: "/assets/player/run1.png" image: "/assets/player/run2.png" image: "/assets/player/run3.png" // More player animations... } ``` ### Animation Culling Only play animations for on-screen objects: ```lua function update(self, dt) -- Check if object is visible local pos = go.get_position() local camera_pos = go.get_position("camera") local distance = vmath.length(pos - camera_pos) if distance > 1000 then -- Stop animation if too far sprite.play_flipbook("#sprite", "idle") self.animating = false elseif not self.animating then -- Resume animation when visible sprite.play_flipbook("#sprite", "run") self.animating = true end end ``` ## Physics Optimization ### Physics Scale Maintain physics objects at an appropriate scale. Defold's physics engine works best with objects between 0.1 and 10 units: ```lua -- Scale factor for converting between game units and physics units local PHYSICS_SCALE = 0.1 -- Apply scale when creating physics objects local scale = vmath.vector3(PHYSICS_SCALE, PHYSICS_SCALE, PHYSICS_SCALE) go.set_scale(scale, "physics_object") ``` ### Dynamic vs Static Bodies Use static bodies whenever possible: ``` components { id: "script" component: "/scripts/platform.script" } embedded_components { id: "collisionobject" type: "collisionobject" data: "collision_shape: \"/assets/platform.convexshape\"\n" "type: COLLISION_OBJECT_TYPE_STATIC\n" -- Static is more efficient than dynamic "mass: 0.0\n" "friction: 0.5\n" "restitution: 0.0\n" "group: \"ground\"\n" "mask: \"player\"\n" "embedded_collision_shape {\n" " shapes {\n" " shape_type: TYPE_BOX\n" " position {\n" " x: 0.0\n" " y: 0.0\n" " z: 0.0\n" " }\n" " rotation {\n" " x: 0.0\n" " y: 0.0\n" " z: 0.0\n" " w: 1.0\n" " }\n" " index: 0\n" " count: 3\n" " }\n" "}\n" "linear_damping: 0.0\n" "angular_damping: 0.0\n" "locked_rotation: false\n" } ``` ### Collision Groups and Masks Use collision groups and masks to prevent unnecessary collision checks: ```lua -- Example collision groups local GROUP_PLAYER = hash("player") local GROUP_ENEMY = hash("enemy") local GROUP_GROUND = hash("ground") local GROUP_PICKUP = hash("pickup") -- In player collision object -- mask: "ground,enemy,pickup" -- In enemy collision object -- mask: "player,ground" -- In ground collision object -- mask: "player,enemy" -- In pickup collision object -- mask: "player" ``` ## Memory Management ### Resource Management Unload unused resources: ```lua function unload_level(level_name) -- Unload resources resource.release("/assets/levels/" .. level_name .. ".collectionc") end ``` ### Table Reuse Reuse tables instead of creating new ones: ```lua -- Bad approach (creates garbage) function update(self, dt) local new_position = vmath.vector3(x, y, z) go.set_position(new_position) end -- Better approach (reuses table) function init(self) self.position = vmath.vector3() end function update(self, dt) self.position.x = x self.position.y = y self.position.z = z go.set_position(self.position) end ``` ## Profiling and Benchmarking ### Built-in Profiler Use Defold's built-in profiler to identify bottlenecks: ```lua function init(self) profiler.enable_ui(true) end ``` ### Custom Performance Metrics Implement custom performance tracking: ```lua local function measure_time(func, ...) local start = socket.gettime() func(...) local end_time = socket.gettime() return (end_time - start) * 1000 -- ms end local time_taken = measure_time(expensive_function) print("Function took " .. time_taken .. " ms") ``` ### Performance Visualization Visualize frame times: ```lua function init(self) self.frame_times = {} self.max_frames = 100 self.current_frame = 1 end function update(self, dt) -- Record frame time self.frame_times[self.current_frame] = dt self.current_frame = (self.current_frame % self.max_frames) + 1 -- Draw frame time graph if debug mode is on if self.debug_mode then draw_frame_time_graph(self.frame_times) end end ``` ## Platform-Specific Optimizations ### Mobile Optimizations For mobile platforms: 1. **Reduce draw calls** - Aim for under 50 draw calls per frame 2. **Limit particle effects** - Use fewer particles on mobile 3. **Texture sizes** - Keep textures at power-of-two sizes (e.g., 256x256, 512x512) 4. **LOD (Level of Detail)** - Implement simpler models/effects for distant objects ```lua function init(self) -- Detect platform if sys.get_sys_info().system_name == "Android" or sys.get_sys_info().system_name == "iPhone OS" then -- Apply mobile optimizations self.particle_count = 20 -- Fewer particles self.draw_distance = 800 -- Shorter draw distance else -- Desktop settings self.particle_count = 100 self.draw_distance = 1500 end end ``` ### Performance Tiers Implement different quality settings based on device performance: ```mermaid flowchart TD A[Start Game] --> B{Detect Device} B -->|Low-End| C[Low Quality Settings] B -->|Mid-Range| D[Medium Quality Settings] B -->|High-End| E[High Quality Settings] C --> F[Apply Settings] D --> F E --> F F --> G[Start Gameplay] ``` ```lua function detect_performance_tier() local info = sys.get_sys_info() local device_model = info.device_model local system_name = info.system_name local cores = info.core_count or 2 if cores <= 2 or is_low_end_device(device_model) then return "low" elseif cores <= 4 then return "medium" else return "high" end end function apply_quality_settings(tier) if tier == "low" then -- Low quality settings particlefx.set_max_particle_count(100) set_render_distance(500) elseif tier == "medium" then -- Medium quality settings particlefx.set_max_particle_count(300) set_render_distance(1000) else -- High quality settings particlefx.set_max_particle_count(500) set_render_distance(1500) end end ``` ## Mathematical Optimizations When performing complex calculations, consider mathematical optimizations: ### Fast Distance Approximation For distance checks where exact values aren't needed: $$\text{distance}^2 = (x_2 - x_1)^2 + (y_2 - y_1)^2$$ ```lua function fast_distance_squared(x1, y1, x2, y2) local dx = x2 - x1 local dy = y2 - y1 return dx * dx + dy * dy end -- Use squared distance for comparisons if fast_distance_squared(player_x, player_y, enemy_x, enemy_y) < 10000 then -- 100^2 -- Enemy is within 100 units of player end ``` ### Lookup Tables For expensive trigonometric functions: ```lua -- Pre-calculate sine values local sin_table = {} for i = 0, 360 do sin_table[i] = math.sin(math.rad(i)) end -- Use lookup instead of calculating sine function fast_sin(angle_degrees) local index = math.floor(angle_degrees % 360) + 1 return sin_table[index] end ``` By implementing these optimization techniques, your Defold platformer will run efficiently across a wide range of devices, providing a smooth and responsive player experience.