FlatRedball games can be distributed on Steam with or without adding code to handle Steam integration. Steam integration can be added using the Steamworks.NET library. This includes support for achievements and responding to the Steam overlay being shown.
If you are targeting .NET 6, use the 64 bit version.
If you are making a Desktop GL (.NET Framework) project, be sure to link to the x86 version as this version of FlatRedBall does not support 64 bit builds
Copy the steam_api64.dll or steam_api.dll file to the same folder as your game's .csproj (and .gluj) depending on whether you linked to the 64 bit version of Steamworks.NET.dll
Add the steam_api file to your project in Visual Studio and mark it as Copy if Newer so that the file ends up in your game's bin folder next to the built .exe.
Add your steam_appid.txt file to the folder where your game's exe is located.
When testing, be sure to have Steam running or else your tests won't work.
Adding SteamManager
Once the Steamworks library is added to your project, you can interact directly with the library to award achievements and respond to the tab overlay being shown. If you would like to work directly with this library, you can find additional information on the Steamworks github page. The documentation is focused on Unity but many of the concepts apply. Alternatively, the following SteamManager class can be used to set up a project quickly. Note that this is provided to help get a project set up quickly. Future versions of FlatRedBall may provide more integrated solutions such as code gen:
#regionAchievement ClassabstractclassAchievementBase { }classBoolAchievement:AchievementBase {string AchievementName;Func<bool> GetCurrentValue;publicBoolAchievement(string achievementName,Func<bool> getCurrentValue) { AchievementName = achievementName; GetCurrentValue = getCurrentValue; }publicvoidTryApply() {if(GetCurrentValue()) {SteamManager.Self.AwardAchievement(AchievementName); } } }classNumericAchievement:AchievementBase {string ProgressStat;Func<long> GetCurrentValue;publicNumericAchievement(string progressStat,Func<long> getCurrentValue) { ProgressStat = progressStat; GetCurrentValue = getCurrentValue; }publicvoidTryApply() {SteamManager.Self.SetStat(ProgressStat,GetCurrentValue()); } }#endregion#regionAchievements listclassAchievements { //// start level select screen //public static NumericAchievement ExampleAchievement = new NumericAchievement( // "star_one_count", // This is the variable of the achievement // () => MyGameObject.GetCurrentValue() // This is value that the player has obtained so far, like the number of powerups collected
// ); }#endregion#regionSteamManagerclassSteamManager:IManager {staticSteamManager self;publicstaticSteamManager Self {get {if (self ==null) self =newSteamManager();return self; } }staticbool isInitialized;staticAppId_t appId;staticCallback<GameOverlayActivated_t> gameOverlayActivatedCallback;staticCallback<UserStatsReceived_t> userStatsReceivedCallback;staticCallback<UserStatsStored_t> userStatsStoredCallback;staticCallback<UserAchievementStored_t> userAchievementStoredCallback;publicstaticAction<bool> SteamOverlayVisibilityChanged;publicvoidInitialize() { // this requires steam_appid.txt in the bin folder, and also that Steam is running isInitialized =Steamworks.SteamAPI.Init();if (isInitialized) { //var name = Steamworks.SteamFriends.GetPersonaName(); appId =Steamworks.SteamUtils.GetAppID();SteamUserStats.RequestCurrentStats(); gameOverlayActivatedCallback =Callback<GameOverlayActivated_t>.Create(HandleOverlayActivated); userStatsReceivedCallback =Callback<UserStatsReceived_t>.Create(HandleUserStatsReceived); userStatsStoredCallback =Callback<UserStatsStored_t>.Create(HandleUserStatsStored); userAchievementStoredCallback =Callback<UserAchievementStored_t>.Create(HandleUserAchievementStored); // Example: Gets an achievement by ID //var achievement = SteamUserStats.GetAchievementName(2); // Example: Gets the number of achievemtns the user has been awarded: //var achievementCount = SteamUserStats.GetNumAchievements(); } }publicvoidAwardAchievement(string achievementId) {if (isInitialized) {SteamUserStats.SetAchievement(achievementId); } }publicvoidSetStat(string statId,long value) {if (isInitialized) {var clamped = (int)(Math.Min(value,int.MaxValue));SteamUserStats.SetStat(statId, clamped); } }privatestaticvoidHandleUserAchievementStored(UserAchievementStored_t param) { }privatestaticvoidHandleUserStatsStored(UserStatsStored_t param) { }privatestaticvoidHandleUserStatsReceived(UserStatsReceived_t param) { }privatestaticvoidHandleOverlayActivated(GameOverlayActivated_t param) {SteamOverlayVisibilityChanged?.Invoke(param.m_bActive>0); }publicvoidUpdate() {if(isInitialized) {Steamworks.SteamAPI.RunCallbacks(); }#if DEBUG // If you want to test awarding achievements, try this: //var keyboard = FlatRedBall.Input.InputManager.Keyboard; //if (keyboard.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift)) //{ // if (keyboard.KeyPushed(Microsoft.Xna.Framework.Input.Keys.D1)) // { // ResetAllStats(); // } // if (keyboard.KeyPushed(Microsoft.Xna.Framework.Input.Keys.D2)) // { //AwardAchievement(Achievements.Destroy1_5Base); //AwardAchievement(Achievements.Research15Creatures); //AwardAchievement(Achievements.Research30Creatures); //AwardAchievement(Achievements.Research45Creatures); // } //}#endif }internalstaticvoidStoreStats() {if (isInitialized) {SteamUserStats.StoreStats(); } }#if DEBUGprivatestaticvoidResetAllStats() {constbool resetAchievements =true;SteamUserStats.ResetAllStats(resetAchievements); }#endifinternalvoidExit() {SteamAPI.Shutdown(); }voidIManager.UpdateDependencies(){} }#endregion
steam_appid.txt
The steam_appid.txt file is a text file which is added to the same location as your game's .exe file. It is a text file which should contain only your app ID (which is a 7 digit number at the time of this writing, but may increase to 8 or 9 digits in the future). Note that creating the file in Visual Studio may add a byte order mark which makes your file unreadable by the Steam api, so create the file as a plain text file through Windows Explorer.
SteamManager Setup
To use the SteamManager:
Add SteamManager.Self.Initialize(); to Game1 constructor
Add SteamManager.Self.Update(); to Game1 Update
Add SteamManager.Self.Exit(); to Game1 OnExiting (you may need to manually override this method in your game)
Handling SteamManager Steam Overlay
Normally games should be paused when the Steam overlay is shown. Games which use GameScreen as their base class for all levels can respond to the SteamManager's SteamOverlayVisibilityChanged event by pausing. For example, the following code snippet could be used to pause the game:
voidCustomInitialize(){...SteamManager.SteamOverlayVisibilityChanged+= HandleSteamOverlayVisibilityChange;}privatevoidHandleSteamOverlayVisibilityChange(bool isSteamOverlayVisible){if(isSteamOverlayVisible &&!IsPaused) { // This will pause the screen, but you may want to call your own custom pause function to handle showing menus // or other game-specific logicPauseThisScreen(); }}voidCustomDestroy(){SteamManager.SteamOverlayVisibilityChanged-= HandleSteamOverlayVisibilityChange;}
Defining Achievements
Steam achievements are handled in two places:
The achievements must be defined in the Steam dashboard for your game
Achievement logic must be added to your game
If using the SteamManager, the second point is fairly easy to do:
Find the Achievements class in the code above
Follow the example achievement to create your own achievement. Note that this pattern requires access to the game data, such as the profile information which may have values controlling whether an achievement has been fulfilled
In game code, call TryApply on achievements which may be achieved in response to certain game events. You may choose to award achievements the moment they are awarded, or at certain points of the game execution (such as the end of a level)
For example, consider an achievement which is awarded when the player has collected all possible power-ups in a game. This may be checked in the collision function between PlayerList and PowerUpList as follows:
Achievements.PowerUpCollection.TryApply();
The TryApply method performs a local check for awarding before sending anything to Steam, so making these calls frequently will typically not cause performance problems. Of course, be aware of situations where the checking of an achievement requires time intensive checks, such as loading files from disk or performing a large number of calculations.
.NET 6 Self Contained Builds
If your game uses .NET 6 or newer, then it can be published as a self-contained app which includes all of the .NET 6 runtime files. While this increases the size of your game, it enables your game to run on any machine regardless of whether .NET 6 runtime is installed. Furthermore, it allows your game to run on SteamDeck. To do this, first add the following highlighted text to your csproj under the PropertyGroup tag.
Once you have done this, you can publish your application using the dotnet publish command or you can grab the files from the bin folder that Visual Studio creates.