12 KiB
Game Optimization Strategy
Current Performance Analysis
Polling Overhead (Every 5 seconds)
Current polling fetches 5 endpoints simultaneously:
/api/game/state- ~1.3KB+ (inventory, equipment, dropped items, player data)/api/game/location- Location data/api/game/profile- Player profile/api/game/combat- Combat status/api/game/pvp/status- PvP combat status
Problems:
- Inventory sent every poll (~1.3KB) even though it rarely changes
- Equipment sent every poll even though it rarely changes
- Dropped items fetched every poll even though location doesn't change often
- Duplicate data across endpoints (player data in multiple responses)
Optimization Strategy
Phase 1: Smart Polling - Only Fetch What Changes
A. Lightweight Polling Endpoint (Every 5s)
Create /api/game/state/minimal that returns ONLY data that changes frequently:
{
"player": {
"hp": 100,
"stamina": 95,
"location_id": "ruins_entrance",
"movement_cooldown": 2
},
"in_combat": false,
"in_pvp_combat": false,
"location_changed": false, // Flag if location changed
"inventory_changed": false, // Flag if inventory changed
"equipment_changed": false, // Flag if equipment changed
"last_update_timestamp": 1699380123.456
}
Size estimate: ~200 bytes vs current 1.5KB+ = 87% reduction
B. On-Demand Fetching
Only fetch full data when flags indicate changes or after specific actions:
Inventory - Fetch only when:
inventory_changedflag is true- After pickup/drop/craft/use actions
- After equipment changes
- On initial load
Equipment - Fetch only when:
equipment_changedflag is true- After equip/unequip actions
- On initial load
Location Details - Fetch only when:
location_changedflag is true- After movement
- On initial load
Dropped Items - Fetch only when:
- Location changes
- After dropping items
- After picking up items
C. Change Tracking in Database
Add columns to players table:
ALTER TABLE players ADD COLUMN inventory_version INTEGER DEFAULT 0;
ALTER TABLE players ADD COLUMN equipment_version INTEGER DEFAULT 0;
ALTER TABLE players ADD COLUMN last_state_change FLOAT DEFAULT 0;
Increment versions when:
- Inventory changes:
inventory_version++ - Equipment changes:
equipment_version++
Frontend caches versions and only re-fetches if version changed.
Phase 2: Delta Updates (Advanced)
Pros:
- Even smaller payload (only changes)
- Extremely efficient for large inventories
Cons:
- More complex implementation
- Need to handle edge cases (out-of-sync states)
- Need full refresh mechanism as fallback
Verdict: Not recommended initially
- Current optimization (Phase 1) will give 85%+ bandwidth reduction
- Delta updates add complexity for diminishing returns
- Only consider if player base grows significantly (10k+ concurrent users)
Phase 3: WebSocket for Real-Time Updates (Future)
Instead of polling, use WebSocket for:
- Push notifications when state changes
- Real-time PvP combat updates
- Instant location updates from other players
- Server broadcasts (events, announcements)
Benefits:
- Zero polling overhead
- True real-time experience
- Server can push updates only when needed
Implementation complexity: Medium-High
Immediate Action Plan
1. Create Minimal State Endpoint (1-2 hours)
File: api/main.py
@app.get("/api/game/state/minimal")
async def get_minimal_state(current_user: dict = Depends(get_current_user)):
"""Lightweight endpoint for polling - returns only frequently changing data"""
player = await db.get_player_by_id(current_user['id'])
# Calculate movement cooldown
import time
current_time = time.time()
last_movement = player.get('last_movement_time', 0)
movement_cooldown = max(0, 5 - (current_time - last_movement))
# Check if data changed since last poll
# Client sends last known versions, server returns flags
inventory_version = player.get('inventory_version', 0)
equipment_version = player.get('equipment_version', 0)
return {
"player": {
"hp": player['hp'],
"max_hp": player['max_hp'],
"stamina": player['stamina'],
"max_stamina": player['max_stamina'],
"location_id": player['location_id'],
"movement_cooldown": int(movement_cooldown),
"is_dead": player.get('is_dead', False)
},
"versions": {
"inventory": inventory_version,
"equipment": equipment_version
},
"timestamp": current_time
}
2. Update Frontend Polling Logic (2-3 hours)
File: pwa/src/components/Game.tsx
// Cache for full data
const [cachedInventory, setCachedInventory] = useState(null)
const [cachedEquipment, setCachedEquipment] = useState(null)
const [lastVersions, setLastVersions] = useState({ inventory: 0, equipment: 0 })
const fetchMinimalState = async () => {
const res = await api.get('/api/game/state/minimal')
// Update HP/Stamina/Location immediately
setPlayerState(prev => ({
...prev,
health: res.data.player.hp,
stamina: res.data.player.stamina,
location_id: res.data.player.location_id
}))
// Check if inventory changed
if (res.data.versions.inventory !== lastVersions.inventory) {
await fetchFullInventory()
setLastVersions(prev => ({ ...prev, inventory: res.data.versions.inventory }))
}
// Check if equipment changed
if (res.data.versions.equipment !== lastVersions.equipment) {
await fetchFullEquipment()
setLastVersions(prev => ({ ...prev, equipment: res.data.versions.equipment }))
}
}
3. Add Version Tracking to Database Functions (1 hour)
Update these functions to increment versions:
add_item_to_inventory()- incrementinventory_versionremove_inventory_item()- incrementinventory_versionequip_item()- incrementequipment_versionunequip_item()- incrementequipment_version
4. Keep Full State Fetch for Actions (Already done!)
After actions (pickup, craft, equip, etc.), fetch full data:
await handlePickup(itemId)
await fetchFullInventory() // Refresh immediately after action
Expected Performance Gains
Bandwidth Reduction
- Current: 1.5KB every 5s = 18KB/minute per player
- Optimized: 200 bytes every 5s = 2.4KB/minute per player
- Savings: 87% reduction in polling bandwidth
Server Load Reduction
- Database queries per poll:
- Current: 8-12 queries (inventory items, equipment, dropped items, etc.)
- Optimized: 1 query (player state only)
- CPU usage: ~80% reduction per poll
- Memory: Significant reduction from not loading/serializing inventory every poll
Scalability
- Current: ~100 concurrent users max
- Optimized: ~800+ concurrent users with same resources
Steam Integration & Monetization
Can you release on Steam?
YES! Your game architecture is perfect for Steam:
- Progressive Web App can be packaged as desktop app
- Electron or Tauri wrapper around your PWA
- Steamworks SDK integration is straightforward
Integration Steps
1. Package as Desktop App (Choose one)
Option A: Electron (Most common)
{
"name": "echoes-of-the-ashes",
"main": "main.js",
"dependencies": {
"electron": "^27.0.0"
}
}
Option B: Tauri (More efficient, Rust-based)
- Smaller bundle size
- Better performance
- Rust backend integration
2. Integrate Steamworks
// Steam initialization
const steamworks = require('steamworks.js')
// Initialize Steam client
if (steamworks.init()) {
const steamId = steamworks.localplayer.getSteamId()
const username = steamworks.localplayer.getName()
// Use Steam ID for authentication
await api.post('/api/auth/steam', {
steamId,
username,
ticket: steamworks.auth.getSessionTicket()
})
}
3. Two-Version Strategy (Free vs Premium)
Database Schema:
ALTER TABLE players ADD COLUMN account_type VARCHAR(20) DEFAULT 'free';
-- Values: 'free', 'steam_premium', 'web_premium'
ALTER TABLE players ADD COLUMN steam_id VARCHAR(50) UNIQUE;
ALTER TABLE players ADD COLUMN premium_expires_at FLOAT;
Backend Restrictions:
@app.post("/api/game/move")
async def move(req, current_user):
player = await db.get_player_by_id(current_user['id'])
# Check premium restrictions
if player['account_type'] == 'free':
if player['level'] >= 3:
raise HTTPException(
status_code=402,
detail="Level 3 is the maximum for free accounts. Upgrade to premium to continue!"
)
# Check location restrictions
if location_id in PREMIUM_LOCATIONS:
raise HTTPException(
status_code=402,
detail="This area is only accessible to premium accounts."
)
# Continue with move logic...
Monetization Options:
-
Steam Purchase (One-time)
- Buy on Steam = Permanent premium
- Price: $9.99 - $19.99
- Steam handles payment, you get 70%
-
Web Subscription
- Stripe/PayPal integration
- $4.99/month or $39.99/year
- Separate from Steam version
-
Hybrid Model
def is_premium(player): # Steam premium (permanent) if player['account_type'] == 'steam_premium': return True # Web premium (subscription) if player['account_type'] == 'web_premium': if player['premium_expires_at'] > time.time(): return True return False
Steam Features You Can Use
-
Achievements
steamworks.achievement.activate('FIRST_COMBAT_WIN') -
Cloud Saves
- Sync player state across devices
- Automatic backups
-
Leaderboards
steamworks.leaderboards.uploadScore('PLAYER_LEVEL', player.level) -
Friends/Multiplayer
- See Steam friends in-game
- Invite to PvP combat
- Group features
-
Workshop
- User-created content
- Custom locations/items
- Community maps
Legal Considerations
-
Steam Agreement
- Need business entity (or individual) to sign Steamworks agreement
- Need Tax ID (EIN for business, SSN for individual)
-
Separate Versions
- ✅ ALLOWED: Selling Steam version + separate web subscription
- ❌ NOT ALLOWED: Forcing Steam users to pay again on web
- ✅ ALLOWED: Different pricing models
- ✅ ALLOWED: Web has features Steam doesn't (and vice versa)
-
Revenue Sharing
- Steam: 30% cut (you get 70%)
- Web direct: 100% yours (minus payment processor ~3%)
Recommended Strategy
Phase 1: Optimize & Test (Current)
- Implement polling optimizations
- Test with small player base
- Gather feedback
Phase 2: Steam Preparation (2-4 weeks)
- Package as Electron app
- Integrate Steamworks SDK
- Implement premium restrictions
- Add achievements/leaderboards
Phase 3: Soft Launch (Steam Early Access)
- Launch as Early Access ($14.99)
- Gather Steam community feedback
- Regular updates
Phase 4: Dual Platform (After Steam is stable)
- Keep Steam version
- Launch web version with subscription
- Cross-platform support (shared servers)
Priority Order
- ✅ Immediate: Polling optimization (biggest impact, low effort)
- ⏳ Short-term: Premium restrictions system (prep for monetization)
- ⏳ Medium-term: Steam integration (packaging + Steamworks)
- 🔮 Long-term: WebSocket real-time updates (if needed)
Would you like me to start implementing the polling optimization now?