Steam Integration
The Popcorning Guinea Pig engine provides built-in Steam integration through Steamworks.NET, enabling your games to access Steam platform features like user profiles, achievements, friends, and social features.
Overview
The Steam integration consists of two main components:
- Player System: Handles Steam user data and avatars
- Steamworks Services: Provides access to Steam platform APIs
Prerequisites
Steamworks SDK Setup
- Register Your Game: Create an app on Steamworks Partner Portal
- Get App ID: Note your Steam App ID for configuration
- Download SDK: The engine includes Steamworks.NET 2025.164.1 (Steamworks SDK 1.65). Games must ship matching
steam_api*natives from that SDK — the wrapper does not.
Steam Client Requirements
- Steam client must be running on the user's machine
- Game must be launched through Steam (for production)
- For development: Steam client running +
steam_appid.txtfile
Player System
SteamPlayer Class
The SteamPlayer class extends the base Player class with Steam-specific functionality:
using PopcorningGuineaPig.Engine.Gameplay;
using Steamworks;
public class MyGame : Game
{
private SteamPlayer localPlayer;
public override void Load()
{
// Initialize Steam (must be done before creating SteamPlayer)
if (SteamAPI.Init())
{
// Create Steam player with current user's data
localPlayer = new SteamPlayer();
Console.WriteLine($"Welcome, {localPlayer.Name}!");
Console.WriteLine($"Steam ID: {localPlayer.Id}");
}
else
{
Console.WriteLine("Failed to initialize Steam API");
// Fall back to regular Player
localPlayer = new Player() { Name = "Guest" };
}
}
protected override void CleanupResources()
{
SteamAPI.Shutdown();
}
}
Player Properties
The SteamPlayer provides these properties:
public class SteamPlayer : Player
{
public CSteamID Id { get; set; } // Steam ID of the player
public string Name { get; set; } // Steam persona name
public byte[] AvatarImageData { get; } // Avatar image as RGBA bytes
public int AvatarWidth { get; } // Avatar image width
public int AvatarHeight { get; } // Avatar image height
}
Avatar Integration
Steam avatars are automatically retrieved and can be used in your UI:
public class ProfileScene : Scene
{
private SteamPlayer player;
private Texture2D avatarTexture;
public override void Load()
{
player = new SteamPlayer();
// Convert avatar data to Raylib texture
if (player.AvatarImageData != null)
{
// Create Raylib Image from raw RGBA data
var image = new Image
{
data = player.AvatarImageData,
width = player.AvatarWidth,
height = player.AvatarHeight,
format = PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
mipmaps = 1
};
// Convert to texture for rendering
avatarTexture = Raylib.LoadTextureFromImage(image);
Raylib.UnloadImage(image);
}
}
public override void Render(int width, int height)
{
// Draw player name
Raylib.DrawText(player.Name, 50, 50, 24, Color.WHITE);
// Draw avatar if available
if (avatarTexture.id != 0)
{
Raylib.DrawTexture(avatarTexture, 50, 80, Color.WHITE);
}
}
public override void Unload()
{
if (avatarTexture.id != 0)
{
Raylib.UnloadTexture(avatarTexture);
}
}
}
Steam API Integration
Friends System
Access Steam friends list and their information:
public class FriendsManager
{
public List<SteamPlayer> GetOnlineFriends()
{
var friends = new List<SteamPlayer>();
int friendCount = SteamFriends.GetFriendCount(EFriendFlags.k_EFriendFlagImmediate);
for (int i = 0; i < friendCount; i++)
{
CSteamID friendId = SteamFriends.GetFriendByIndex(i, EFriendFlags.k_EFriendFlagImmediate);
// Check if friend is online
if (SteamFriends.GetFriendPersonaState(friendId) != EPersonaState.k_EPersonaStateOffline)
{
var friend = new SteamPlayer(friendId);
friends.Add(friend);
}
}
return friends;
}
public void InviteFriendToGame(CSteamID friendId)
{
// Send game invite to friend
SteamFriends.InviteUserToGame(friendId, "+connect_lobby");
}
}
Rich Presence
Update your game's status on Steam:
public class GamePresence
{
public void SetMainMenuPresence()
{
SteamFriends.SetRichPresence("status", "In Main Menu");
SteamFriends.SetRichPresence("steam_display", "#Status");
}
public void SetInGamePresence(string levelName, int playerCount)
{
SteamFriends.SetRichPresence("status", $"Playing {levelName}");
SteamFriends.SetRichPresence("players", playerCount.ToString());
SteamFriends.SetRichPresence("steam_display", "#Status_WithPlayers");
}
public void ClearPresence()
{
SteamFriends.ClearRichPresence();
}
}
Steam Achievements
Implement Steam achievements in your game:
public class AchievementManager
{
private Dictionary<string, bool> achievementStatus = new();
public void Initialize()
{
// Load current achievement status
LoadAchievements();
}
public void UnlockAchievement(string achievementName)
{
if (!achievementStatus.ContainsKey(achievementName) || !achievementStatus[achievementName])
{
// Unlock on Steam
SteamUserStats.SetAchievement(achievementName);
SteamUserStats.StoreStats();
achievementStatus[achievementName] = true;
Console.WriteLine($"Achievement unlocked: {achievementName}");
}
}
public bool IsAchievementUnlocked(string achievementName)
{
return achievementStatus.ContainsKey(achievementName) && achievementStatus[achievementName];
}
private void LoadAchievements()
{
// SDK 1.61+: stats are ready after SteamAPI.Init; RequestCurrentStats is gone.
}
// Call this in your game's update loop
public void HandleCallbacks()
{
// Process Steam callbacks
SteamAPI.RunCallbacks();
}
}
Steam Statistics
Track and submit game statistics:
public class GameStats
{
public void RecordGamePlayed()
{
// Increment games played stat
int currentGames = 0;
SteamUserStats.GetStat("games_played", out currentGames);
SteamUserStats.SetStat("games_played", currentGames + 1);
SteamUserStats.StoreStats();
}
public void RecordScore(int score)
{
// Track high score
int highScore = 0;
SteamUserStats.GetStat("high_score", out highScore);
if (score > highScore)
{
SteamUserStats.SetStat("high_score", score);
SteamUserStats.StoreStats();
}
}
public void RecordPlayTime(float seconds)
{
// Add to total play time
float totalTime = 0;
SteamUserStats.GetStat("total_playtime", out totalTime);
SteamUserStats.SetStat("total_playtime", totalTime + seconds);
SteamUserStats.StoreStats();
}
}
Steam Cloud Save
Save and load game data to Steam Cloud:
public class CloudSaveManager
{
public bool SaveToCloud(string filename, byte[] data)
{
try
{
// Write data to Steam Cloud
bool success = SteamRemoteStorage.FileWrite(filename, data, data.Length);
if (success)
{
Console.WriteLine($"Saved {filename} to Steam Cloud ({data.Length} bytes)");
}
return success;
}
catch (Exception ex)
{
Console.WriteLine($"Failed to save to Steam Cloud: {ex.Message}");
return false;
}
}
public byte[] LoadFromCloud(string filename)
{
try
{
if (SteamRemoteStorage.FileExists(filename))
{
int fileSize = SteamRemoteStorage.GetFileSize(filename);
byte[] data = new byte[fileSize];
int bytesRead = SteamRemoteStorage.FileRead(filename, data, fileSize);
if (bytesRead == fileSize)
{
Console.WriteLine($"Loaded {filename} from Steam Cloud ({bytesRead} bytes)");
return data;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to load from Steam Cloud: {ex.Message}");
}
return null;
}
public void SyncCloudData()
{
// Force synchronization with Steam Cloud
// This is usually automatic, but can be forced if needed
Console.WriteLine("Syncing with Steam Cloud...");
}
}
Development Setup
Creating steam_appid.txt
For development, create a steam_appid.txt file in your game's directory:
480
Replace 480 with your actual Steam App ID. This file should be in the same directory as your executable.
Project Configuration
Steamworks.NET arrives with PopcorningGuineaPig.Engine (2025.164.1, SDK 1.65). Do not add a separate PackageReference — nuget.org still only has 2024.8.0 (SDK 1.60), which will not Init against 1.65 natives. Ship Valve's redistributable (steam_api64.dll / libsteam_api.dylib / libsteam_api.so) next to the exe.
The unix wrapper is IL-only but PE-flagged AMD64. The nupkg's runtimes/osx-arm64 copy is retargeted to ARM64 at pack time so net10 on Apple Silicon will load it. Do not patch Steamworks.NET.dll in the game.
Initialization Pattern
Use this pattern for Steam initialization in your game:
public class SteamEnabledGame : Game
{
private bool steamInitialized = false;
private SteamPlayer localPlayer;
private AchievementManager achievements;
public override void Load()
{
// Initialize Steam API
steamInitialized = SteamAPI.Init();
if (steamInitialized)
{
Console.WriteLine("Steam API initialized successfully");
// Initialize Steam-dependent features
localPlayer = new SteamPlayer();
achievements = new AchievementManager();
achievements.Initialize();
// Set initial rich presence
SteamFriends.SetRichPresence("status", "Starting Game");
}
else
{
Console.WriteLine("Steam API failed to initialize - running without Steam features");
localPlayer = new Player { Name = "Player" };
}
// Continue with normal game initialization
InitializeGame();
}
public override void Update(double deltaTime)
{
// Process Steam callbacks
if (steamInitialized)
{
SteamAPI.RunCallbacks();
achievements.HandleCallbacks();
}
// Normal game update
base.Update(deltaTime);
}
protected override void CleanupResources()
{
if (steamInitialized)
{
SteamFriends.ClearRichPresence();
SteamAPI.Shutdown();
}
base.CleanupResources();
}
}
Platform Integration UI
Create Steam-aware UI components:
public class SteamProfilePanel : Panel
{
private SteamPlayer player;
private Label nameLabel;
private Button friendsButton;
public SteamProfilePanel(SteamPlayer player, Vector2 position, Vector2 size) : base(position, size)
{
this.player = player;
Setup();
}
private void Setup()
{
// Player name label
nameLabel = new Label(
text: player.Name,
position: new Vector2(GetPosition().X + 10, GetPosition().Y + 10),
fontSize: 18,
color: Color.WHITE
);
// Friends button
friendsButton = new Button(
position: new Vector2(GetPosition().X + 10, GetPosition().Y + 40),
size: new Vector2(100, 30),
text: "View Friends"
);
friendsButton.SetOnClick(() => {
// Open Steam overlay to friends list
SteamFriends.ActivateGameOverlay("friends");
});
// Add to panel
AddChild(nameLabel);
AddChild(friendsButton);
}
public override void Update()
{
base.Update();
friendsButton.Update();
}
public override void Draw()
{
base.Draw();
nameLabel.Draw();
friendsButton.Draw();
}
}
Error Handling
Proper error handling for Steam integration:
public class SteamErrorHandler
{
public static bool SafeInitialize()
{
try
{
if (!SteamAPI.Init())
{
Console.WriteLine("Steam API initialization failed:");
Console.WriteLine("- Steam client may not be running");
Console.WriteLine("- Invalid App ID in steam_appid.txt");
Console.WriteLine("- Steamworks SDK not properly configured");
return false;
}
// Verify user is logged in
if (!SteamUser.BLoggedOn())
{
Console.WriteLine("User is not logged into Steam");
return false;
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Steam initialization exception: {ex.Message}");
return false;
}
}
public static void HandleSteamCallback(CallbackMsg_t callback)
{
// Handle Steam callback errors
switch (callback.m_iCallback)
{
case SteamAPICallCompleted_t.k_iCallback:
// Handle API call completion
break;
case UserStatsReceived_t.k_iCallback:
// Handle user stats received
break;
default:
Console.WriteLine($"Unhandled Steam callback: {callback.m_iCallback}");
break;
}
}
}
Best Practices
Performance Considerations
- Callback Processing: Always call
SteamAPI.RunCallbacks()regularly - Avatar Caching: Cache avatar textures to avoid repeated conversions
- Async Operations: Don't block the main thread for Steam operations
Security
- Never Trust Client Data: Always validate Steam data server-side for multiplayer
- Handle Offline Mode: Gracefully degrade when Steam is unavailable
- Privacy Awareness: Respect user privacy settings
User Experience
- Fallback Gracefully: Provide functionality even when Steam is unavailable
- Clear Feedback: Inform users about Steam-specific features
- Respect Steam UI: Use Steam overlays for Steam-specific interactions
Testing
Development Testing
# Set your App ID
echo "480" > steam_appid.txt
# Run with Steam client open
dotnet run
Integration Testing Checklist
- [ ] Steam API initializes correctly
- [ ] Player data loads properly
- [ ] Avatar images display correctly
- [ ] Rich presence updates work
- [ ] Achievements unlock and save
- [ ] Statistics track correctly
- [ ] Cloud saves work bidirectionally
- [ ] Friends list integration functions
- [ ] Graceful degradation when Steam unavailable
Common Issues
"Steam API failed to initialize"
Causes:
- Steam client not running
- Invalid App ID in
steam_appid.txt - Missing Steamworks.NET package
Solutions:
- Ensure Steam client is running
- Verify App ID is correct
- Check
steam_appid.txtis in the correct location
"Avatar not loading"
Causes:
- User has private profile
- Avatar hasn't downloaded yet
- Network connectivity issues
Solutions:
- Provide default avatar fallback
- Retry avatar fetch after delay
- Check Steam privacy settings
Advanced Features
Steam Workshop Integration
public class WorkshopManager
{
public void SubscribeToItem(PublishedFileId_t itemId)
{
SteamUGC.SubscribeItem(itemId);
}
public void GetSubscribedItems()
{
uint numSubscribed = SteamUGC.GetNumSubscribedItems();
PublishedFileId_t[] subscribedItems = new PublishedFileId_t[numSubscribed];
SteamUGC.GetSubscribedItems(subscribedItems, numSubscribed);
foreach (var item in subscribedItems)
{
// Process subscribed workshop items
ProcessWorkshopItem(item);
}
}
private void ProcessWorkshopItem(PublishedFileId_t itemId)
{
// Handle workshop item download and installation
ulong sizeOnDisk;
string folder;
uint timestamp;
bool installed = SteamUGC.GetItemInstallInfo(itemId, out sizeOnDisk, out folder, 1024, out timestamp);
if (installed)
{
Console.WriteLine($"Workshop item installed at: {folder}");
// Load workshop content from folder
}
}
}
Further Reading
- API Reference - Complete API documentation
- Game Architecture - Player architecture overview
- Settings Management - Steam settings management
- Multiplayer Guide - Steam networking features
Steam integration provides powerful social and platform features that enhance your game's appeal and user engagement. Use these features to create a more connected and social gaming experience.