Last Update: 2023-07-06
Reducing Resource Usage in Mods
This guide focuses on strategies to minimize resource consumption in Don't Starve Together mods, ensuring they run efficiently on a variety of hardware configurations and server environments.
Understanding Resource Constraints
When developing mods for Don't Starve Together, it's important to consider various resource constraints:
- Memory Usage: Excessive memory consumption can lead to game crashes or degraded performance
- CPU Utilization: High CPU usage causes frame rate drops and gameplay lag
- Network Bandwidth: In multiplayer, network traffic can become a bottleneck
- File Size: Large mods take longer to download and may deter players from installing
Memory Optimization Techniques
1. Avoid Table Bloat
Tables in Lua are the primary data structure, but they can consume significant memory if not managed properly:
-- INEFFICIENT: Creating new tables constantly
function OnUpdate()
-- This creates a new table every update
local nearby_entities = {}
-- Fill with data
for i = 1, 100 do
nearby_entities[i] = {
id = i,
position = {x = i, y = 0, z = i},
data = "Entity " .. i
}
end
-- Process data
ProcessEntities(nearby_entities)
-- Table becomes garbage after function ends
end
Optimized Approach:
-- OPTIMIZED: Reuse tables
local nearby_entities = {} -- Defined once outside the function
function OnUpdate()
-- Clear the table instead of creating a new one
for k in pairs(nearby_entities) do
nearby_entities[k] = nil
end
-- Fill with data
for i = 1, 100 do
nearby_entities[i] = nearby_entities[i] or {} -- Reuse existing subtable if available
local entity = nearby_entities[i]
entity.id = i
entity.position = entity.position or {} -- Reuse position table
entity.position.x = i
entity.position.y = 0
entity.position.z = i
entity.data = "Entity " .. i
end
-- Process data
ProcessEntities(nearby_entities)
-- Table is reused in next update
end
2. Implement Object Pooling
For frequently created and destroyed objects, use object pooling to reduce memory churn:
-- Object pool implementation
local ProjectilePool = {
active = {},
inactive = {},
}
function ProjectilePool:Get()
local projectile
if #self.inactive > 0 then
-- Reuse an inactive projectile
projectile = table.remove(self.inactive)
else
-- Create a new projectile if none available
projectile = {
position = {x = 0, y = 0, z = 0},
velocity = {x = 0, y = 0, z = 0},
active_time = 0
}
end
table.insert(self.active, projectile)
return projectile
end
function ProjectilePool:Release(projectile)
for i, p in ipairs(self.active) do
if p == projectile then
table.remove(self.active, i)
table.insert(self.inactive, projectile)
return
end
end
end
-- Usage example
function FireProjectile(start_pos, direction)
local projectile = ProjectilePool:Get()
-- Reset properties
projectile.position.x = start_pos.x
projectile.position.y = start_pos.y
projectile.position.z = start_pos.z
projectile.velocity.x = direction.x * 10
projectile.velocity.y = direction.y * 10
projectile.velocity.z = direction.z * 10
projectile.active_time = 0
end
3. Use Sparse Data Structures
For large data sets where most entries are empty, use sparse data structures:
-- INEFFICIENT: Dense grid representation
local world_grid = {}
for x = -1000, 1000 do
world_grid[x] = {}
for z = -1000, 1000 do
world_grid[x][z] = 0 -- Default value
end
end
-- Set a few values
world_grid[10][20] = 1
world_grid[50][60] = 2
Optimized Approach:
-- OPTIMIZED: Sparse grid representation
local world_grid = {}
-- Helper functions
function SetGridValue(grid, x, z, value)
grid[x] = grid[x] or {}
grid[x][z] = value
end
function GetGridValue(grid, x, z)
return (grid[x] and grid[x][z]) or 0 -- Default value
end
-- Set values only where needed
SetGridValue(world_grid, 10, 20, 1)
SetGridValue(world_grid, 50, 60, 2)
-- Get a value
local value = GetGridValue(world_grid, 10, 20) -- Returns 1
local default_value = GetGridValue(world_grid, 5, 5) -- Returns 0
4. Minimize Closure Creation
Closures that capture large environments can consume significant memory:
-- INEFFICIENT: Creating many closures that capture the environment
function CreateEntityProcessors(entities)
local processors = {}
for i, entity in ipairs(entities) do
-- This creates a new closure for each entity, capturing 'entity'
processors[i] = function()
ProcessEntity(entity)
end
end
return processors
end
Optimized Approach:
-- OPTIMIZED: Use a single function with parameters
function ProcessEntityById(entity_id)
local entity = entities[entity_id]
ProcessEntity(entity)
end
function CreateEntityProcessors(entities)
local processor_ids = {}
for i, entity in ipairs(entities) do
processor_ids[i] = entity.id
end
return processor_ids
end
-- Usage
local ids = CreateEntityProcessors(entities)
for _, id in ipairs(ids) do
ProcessEntityById(id)
end
CPU Optimization Techniques
1. Defer Expensive Calculations
Instead of calculating everything immediately, defer calculations until they're needed:
-- INEFFICIENT: Calculating everything upfront
function UpdateCreature(creature)
-- Calculate all possible paths
local paths = CalculateAllPaths(creature)
-- Calculate visibility for all entities
local visible_entities = CalculateAllVisibleEntities(creature)
-- Calculate all possible interactions
local possible_interactions = CalculateAllInteractions(creature, visible_entities)
-- Actually decide what to do
local chosen_action = ChooseBestAction(creature, paths, visible_entities, possible_interactions)
-- Execute just one action
ExecuteAction(creature, chosen_action)
end
Optimized Approach:
-- OPTIMIZED: Calculate only what's needed
function UpdateCreature(creature)
-- First determine what type of action is needed
local action_type = DetermineActionType(creature)
if action_type == "MOVE" then
-- Only calculate paths if moving
local path = CalculateBestPath(creature)
ExecuteMovement(creature, path)
elseif action_type == "INTERACT" then
-- Only calculate visible entities if interacting
local visible_entities = CalculateVisibleEntities(creature)
local target = ChooseInteractionTarget(creature, visible_entities)
ExecuteInteraction(creature, target)
elseif action_type == "IDLE" then
-- Do minimal work for idle creatures
ExecuteIdleBehavior(creature)
end
end
2. Use Lookup Tables for Expensive Calculations
For calculations that are used repeatedly with the same inputs:
-- INEFFICIENT: Recalculating values repeatedly
function GetDamageMultiplier(weapon_type, target_type)
-- Complex calculation based on weapon and target types
return CalculateDamageMultiplier(weapon_type, target_type)
end
Optimized Approach:
-- OPTIMIZED: Use lookup tables
local damage_multipliers = {
sword = {
spider = 1.5,
skeleton = 1.0,
ghost = 0.5,
-- other target types...
},
spear = {
spider = 1.2,
skeleton = 1.3,
ghost = 0.3,
-- other target types...
},
-- other weapon types...
}
function GetDamageMultiplier(weapon_type, target_type)
-- Fast table lookup instead of calculation
return (damage_multipliers[weapon_type] and damage_multipliers[weapon_type][target_type]) or 1.0
end
3. Throttle Update Frequency Based on Importance
Not all systems need to update at the same frequency:
-- Different update frequencies for different systems
function InitializeUpdateSystem()
-- Critical systems update frequently
TheWorld:DoPeriodicTask(0.1, UpdateCriticalSystems)
-- Important but not critical systems
TheWorld:DoPeriodicTask(0.5, UpdateImportantSystems)
-- Background systems can update slowly
TheWorld:DoPeriodicTask(1.0, UpdateBackgroundSystems)
-- Very infrequent updates for non-critical systems
TheWorld:DoPeriodicTask(5.0, UpdateNonCriticalSystems)
end
4. Implement Dirty Flags
Only update what has changed:
local StatusComponent = Class(function(self, inst)
self.inst = inst
self.values = {
health = 100,
hunger = 100,
sanity = 100
}
self.dirty = {
health = true,
hunger = true,
sanity = true
}
self.inst:DoPeriodicTask(0.5, function() self:Update() end)
end)
function StatusComponent:SetValue(key, value)
if self.values[key] ~= value then
self.values[key] = value
self.dirty[key] = true
end
end
function StatusComponent:Update()
-- Only update what has changed
if self.dirty.health then
self:UpdateHealthDisplay()
self.dirty.health = false
end
if self.dirty.hunger then
self:UpdateHungerDisplay()
self.dirty.hunger = false
end
if self.dirty.sanity then
self:UpdateSanityDisplay()
self.dirty.sanity = false
end
end
Network Bandwidth Optimization
1. Prioritize Network Updates
Not all data needs to be synchronized at the same frequency:
local SyncComponent = Class(function(self, inst)
self.inst = inst
self.last_sync = {
position = 0,
animation = 0,
status = 0
}
self.sync_frequency = {
position = 0.1, -- Position updates frequently
animation = 0.5, -- Animation state less frequently
status = 1.0 -- Status updates infrequently
}
self.inst:DoPeriodicTask(0.1, function() self:TrySyncData() end)
end)
function SyncComponent:TrySyncData()
local current_time = GetTime()
-- Check if it's time to sync each data type
if current_time - self.last_sync.position >= self.sync_frequency.position then
self:SyncPosition()
self.last_sync.position = current_time
end
if current_time - self.last_sync.animation >= self.sync_frequency.animation then
self:SyncAnimation()
self.last_sync.animation = current_time
end
if current_time - self.last_sync.status >= self.sync_frequency.status then
self:SyncStatus()
self.last_sync.status = current_time
end
end
2. Implement Delta Compression
Send only what has changed:
local NetworkSync = Class(function(self, inst)
self.inst = inst
self.last_sent_state = {
position = {x=0, y=0, z=0},
health = 100,
inventory = {}
}
self.inst:DoPeriodicTask(0.5, function() self:SyncState() end)
end)
function NetworkSync:SyncState()
local current_state = self:GetCurrentState()
local delta = self:CalculateDelta(self.last_sent_state, current_state)
-- Only send if there are changes
if next(delta) ~= nil then
self:SendStateDelta(delta)
self.last_sent_state = current_state
end
end
function NetworkSync:CalculateDelta(old_state, new_state)
local delta = {}
-- Check position change
if math.abs(old_state.position.x - new_state.position.x) > 0.1 or
math.abs(old_state.position.z - new_state.position.z) > 0.1 then
delta.position = new_state.position
end
-- Check health change
if old_state.health ~= new_state.health then
delta.health = new_state.health
end
-- Check inventory changes
for item, count in pairs(new_state.inventory) do
if old_state.inventory[item] ~= count then
delta.inventory = delta.inventory or {}
delta.inventory[item] = count
end
end
return delta
end
3. Batch Network Updates
Group multiple updates into a single network message:
local NetworkBatcher = Class(function(self, inst)
self.inst = inst
self.pending_updates = {}
self.last_send_time = 0
self.send_interval = 0.5
self.inst:DoPeriodicTask(0.1, function() self:TrySendBatch() end)
end)
function NetworkBatcher:QueueUpdate(entity_id, data)
self.pending_updates[entity_id] = self.pending_updates[entity_id] or {}
-- Merge the new data with any existing queued data
for k, v in pairs(data) do
self.pending_updates[entity_id][k] = v
end
end
function NetworkBatcher:TrySendBatch()
local current_time = GetTime()
-- Send batch if it's time or if we have too many updates
if current_time - self.last_send_time >= self.send_interval or #self.pending_updates > 10 then
if next(self.pending_updates) ~= nil then
self:SendBatch(self.pending_updates)
self.pending_updates = {}
self.last_send_time = current_time
end
end
end
File Size Optimization
1. Optimize Assets
Reduce the size of included assets:
- Compress textures appropriately
- Use audio formats with good compression
- Consider using texture atlases instead of individual images
- Use lower resolution textures for less important elements
2. Split Mods into Modules
For large mods, consider splitting into core and optional modules:
-- In modmain.lua
local function LoadOptionalModule(name)
local success, module = pcall(require, "modules/" .. name)
if success then
print("Loaded optional module: " .. name)
return module
else
print("Optional module not available: " .. name)
return nil
end
end
-- Core functionality always loaded
local core = require("modules/core")
-- Optional high-resolution textures
local high_res = LoadOptionalModule("highres")
-- Optional additional content
local expansion = LoadOptionalModule("expansion")
3. Use Runtime Generation
Generate content procedurally instead of storing it:
-- INEFFICIENT: Storing many pre-made variations
local tree_variations = {
{branches = {{x=0, y=1}, {x=1, y=2}, {x=-1, y=2}, {x=0.5, y=3}, {x=-0.5, y=3}}},
{branches = {{x=0, y=1}, {x=2, y=1.5}, {x=-1, y=2}, {x=0, y=3}}},
{branches = {{x=0, y=1}, {x=1, y=1}, {x=-1, y=1}, {x=0, y=2}, {x=0, y=3}}},
-- Many more variations...
}
Optimized Approach:
-- OPTIMIZED: Generate variations procedurally
function GenerateTreeVariation(seed)
math.randomseed(seed)
local branches = {{x=0, y=1}} -- Start with trunk
local branch_count = math.random(3, 6)
for i = 1, branch_count do
table.insert(branches, {
x = math.random(-10, 10) / 10,
y = 1 + math.random(0, 20) / 10
})
end
return {branches = branches}
end
-- Generate only when needed
function GetTreeVariation(tree_id)
return GenerateTreeVariation(tree_id)
end
Practical Example: Optimizing a Weather Mod
Let's look at a practical example of optimizing a weather mod that was consuming too many resources:
Original Implementation (Resource-Heavy)
-- Weather effect system with high resource usage
local WeatherSystem = Class(function(self, inst)
self.inst = inst
-- Track all entities in the world
self.affected_entities = {}
-- Create many particle effects
self.particles = {}
for i = 1, 1000 do
local particle = SpawnPrefab("weather_particle")
table.insert(self.particles, particle)
end
-- Update every entity every frame
self.inst:DoPeriodicTask(0, function()
self:UpdateAllEntities()
self:UpdateAllParticles()
end)
-- Network sync every frame
if TheWorld.ismastersim then
self.inst:DoPeriodicTask(0, function()
self:SyncToClients()
end)
end
end)
function WeatherSystem:UpdateAllEntities()
-- Find all entities in the world
local x, y, z = Vector3(ThePlayer.Transform:GetWorldPosition())
local entities = TheSim:FindEntities(x, y, z, 100)
-- Process every entity
for _, entity in ipairs(entities) do
self:ApplyWeatherEffect(entity)
end
end
function WeatherSystem:UpdateAllParticles()
-- Update every particle every frame
for _, particle in ipairs(self.particles) do
self:UpdateParticle(particle)
end
end
function WeatherSystem:SyncToClients()
-- Send full state to all clients
SendModRPCToClients(GetClientModRPC("WeatherMod", "SyncWeather"),
json.encode(self:GetFullState()))
end
Optimized Implementation
-- Optimized weather system
local WeatherSystem = Class(function(self, inst)
self.inst = inst
-- Track entities by distance from player
self.affected_entities = {
near = {}, -- Updated frequently
medium = {}, -- Updated less frequently
far = {} -- Updated rarely
}
-- Particle pool
self.particle_pool = {
active = {},
inactive = {},
max_active = 200 -- Limit active particles
}
-- Create a smaller initial pool
for i = 1, 50 do
local particle = SpawnPrefab("weather_particle")
particle:Hide()
table.insert(self.particle_pool.inactive, particle)
end
-- Update entities at different rates
self.inst:DoPeriodicTask(0.1, function() self:UpdateNearEntities() end)
self.inst:DoPeriodicTask(0.5, function() self:UpdateMediumEntities() end)
self.inst:DoPeriodicTask(2.0, function() self:UpdateFarEntities() end)
-- Update particles in batches
self.inst:DoPeriodicTask(0.1, function() self:UpdateParticleBatch() end)
-- Network sync with throttling
if TheWorld.ismastersim then
self.last_sync = 0
self.sync_interval = 0.5
self.dirty = false
self.inst:DoPeriodicTask(0.1, function() self:TrySyncToClients() end)
end
end)
function WeatherSystem:CategorizeEntity(entity)
local player = ThePlayer
if not player then return "far" end
local dist = entity:GetDistanceSqToPoint(player.Transform:GetWorldPosition())
if dist < 20*20 then
return "near"
elseif dist < 50*50 then
return "medium"
else
return "far"
end
end
function WeatherSystem:UpdateEntityCategories()
local player = ThePlayer
if not player then return end
-- Only recategorize periodically
local x, y, z = player.Transform:GetWorldPosition()
local entities = TheSim:FindEntities(x, y, z, 100, nil, {"INLIMBO"})
-- Clear categories
self.affected_entities.near = {}
self.affected_entities.medium = {}
self.affected_entities.far = {}
-- Categorize entities by distance
for _, entity in ipairs(entities) do
local category = self:CategorizeEntity(entity)
table.insert(self.affected_entities[category], entity)
end
end
function WeatherSystem:UpdateNearEntities()
for _, entity in ipairs(self.affected_entities.near) do
if entity:IsValid() then
self:ApplyWeatherEffect(entity, 1.0) -- Full effect
end
end
end
function WeatherSystem:UpdateMediumEntities()
for _, entity in ipairs(self.affected_entities.medium) do
if entity:IsValid() then
self:ApplyWeatherEffect(entity, 0.7) -- Reduced effect
end
end
-- Recategorize entities periodically
self:UpdateEntityCategories()
end
function WeatherSystem:UpdateFarEntities()
for _, entity in ipairs(self.affected_entities.far) do
if entity:IsValid() then
self:ApplyWeatherEffect(entity, 0.3) -- Minimal effect
end
end
end
function WeatherSystem:GetParticle()
-- Reuse an inactive particle if available
if #self.particle_pool.inactive > 0 then
local particle = table.remove(self.particle_pool.inactive)
particle:Show()
table.insert(self.particle_pool.active, particle)
return particle
end
-- Only create new particles if under the limit
if #self.particle_pool.active < self.particle_pool.max_active then
local particle = SpawnPrefab("weather_particle")
table.insert(self.particle_pool.active, particle)
return particle
end
-- Return nil if at the limit
return nil
end
function WeatherSystem:ReleaseParticle(particle)
for i, p in ipairs(self.particle_pool.active) do
if p == particle then
table.remove(self.particle_pool.active, i)
particle:Hide()
table.insert(self.particle_pool.inactive, particle)
return
end
end
end
function WeatherSystem:UpdateParticleBatch()
-- Process particles in batches
local batch_size = 20
local count = 0
for i = 1, math.min(batch_size, #self.particle_pool.active) do
local particle = self.particle_pool.active[i]
if particle:IsValid() then
if not self:UpdateParticle(particle) then
self:ReleaseParticle(particle)
end
else
self:ReleaseParticle(particle)
end
count = count + 1
end
-- Spawn new particles if needed
local player = ThePlayer
if player and count < batch_size then
local x, y, z = player.Transform:GetWorldPosition()
for i = 1, math.random(1, 3) do
local particle = self:GetParticle()
if particle then
self:InitializeParticle(particle, x, y, z)
end
end
end
end
function WeatherSystem:TrySyncToClients()
if not TheWorld.ismastersim then return end
local current_time = GetTime()
-- Only sync if dirty and not too recent
if self.dirty and current_time - self.last_sync >= self.sync_interval then
-- Send only changed data
SendModRPCToClients(GetClientModRPC("WeatherMod", "SyncWeather"),
json.encode(self:GetDeltaState()))
self.last_sync = current_time
self.dirty = false
end
end
function WeatherSystem:MarkDirty()
self.dirty = true
end
Conclusion
By implementing these resource optimization techniques, your mods will run more efficiently, use less memory, and provide a better experience for players. Remember to:
- Measure First: Profile your mod to identify the biggest resource consumers
- Optimize Strategically: Focus on the areas that will give the biggest improvements
- Test Thoroughly: Ensure optimizations don't introduce new bugs
- Balance Performance and Features: Sometimes it's better to scale back features than to include poorly performing ones
With these approaches, you can create complex, feature-rich mods that still run smoothly on a variety of hardware configurations.
See also
- Performance Optimization - General performance optimization techniques
- Network System - More details on networking in DST
- Event System - Using events efficiently
- Entity System - Working with entities efficiently