Setting up a C# solution for EOS in Visual Studio 2019

Rajen Kishna, Technical Account Manager, Epic Games |
September 28, 2021
In the previous article of this series, I explained how you can register to access the Epic Games Developer Portal to get started using Epic Online Services (EOS). With an account set up, I'll walk you through setting up Visual Studio 2019 and creating a WPF solution with C#, which we'll use to explore all EOS services. Be sure to keep the callouts from the previous article in mind as we progress. In this article we'll cover:
 

Installing Visual Studio 2019

Head over to https://visualstudio.microsoft.com/ to download Visual Studio 2019, if you don't have it installed. Microsoft offers a free Community edition for individuals and certain organizations, so be sure to check out if you qualify (scroll all the way to the bottom). We'll need a few workloads and components, depending on the programming language we'll use:
 
  • C++
    • Desktop & Mobile > Desktop development with C++
      • Under Optional on the right sidebar, be sure to check MSVC v141 - VS2017 C++ x64/x86 build tools (v14.16), as the EOS SDK samples use these build tools.
    • Gaming > Game development with C++
  • C#
    • Desktop & Mobile > .NET desktop development
VS2019 Workloads
Visual Studio 2019 workloads
 
VS2019 MSVC141 Component
Visual Studio 2019 workloads
 

We want to compile and run the EOS SDK C++ and C# samples, so install all workloads and components listed above.

Downloading the SDK

While Visual Studio is installing, you can download the SDK from the Developer Portal. I’ll be using SDK 1.14.1 in these articles, unless otherwise noted.
 
  1. Log in to the Developer Portal at https://dev.epicgames.com/portal/.
  2. Select SDK in the menu on the left. Here you'll find the downloads for the C, C#, iOS, and Android SDK, as well as an overview of your products (including a quick overview to the credentials the SDK will need), and the SDK changelog.
  3. Select the C SDK in the SDK Version drop-down menu and click Download. Do the same for the C# SDK, and other platforms you'd like to explore. We'll focus on C# in this series, but you can apply the same concepts to the other platforms.
  4. Save and extract each SDK in a convenient location.

Each SDK folder will have 3 subfolders: Samples, SDK, and ThirdPartyNotices. The Samples subfolder will have solutions and/or projects for that platform, with the C SDK providing the most comprehensive set of Samples. The SDK subfolder contains binaries, libraries, headers, and sources for the SDK itself, as well as a Tools folder containing the Developer Authentication Tool, File Decryption Tool, and Anti-Cheat Integrity Tool and redistributables. In ThirdPartyNotices you'll find a text file outlining the third party software notices that the SDK uses.

Compiling the SDK Samples

With the SDKs downloaded and Visual Studio installed, we can compile the Samples to quickly verify everything is set up correctly and provide a reference to dig into specific functionality.
 
  1. Start by opening the Samples.sln file from the C SDK Samples folder in Visual Studio 2019.
  2. With the solution open, hit Ctrl+Shift+B to build all projects. At this point, you'll probably see the following error message pop up for each project: 

Error    MSB8036    The Windows SDK version 10.0.17763.0 was not found. Install the required version of Windows SDK or change the SDK version in the project property pages or by right-clicking the solution and selecting "Retarget solution".
 
  1. To resolve these errors, right-click on the solution node in Solution Explorer and click Retarget solution. Here you can select the Windows SDK Version you have installed (in my case 10.0.19041.0) and optionally upgrade the platform toolset to v142. Since we've already installed v141 of the toolset during setup, I'll leave this to not upgrade.
VS2019 Retarget Solution
Retarget solution for the C SDK Samples
 
  1. With the solution retargeted, building with Ctrl+Shift+B should now succeed.
Before we can run any of the Samples, we need to enter our credentials. In the C SDK Samples projects, these can be entered in the Source\SampleConstants.h file in each project. You can quickly find all these values by clicking on the Get Credentials button on the SDK page in the Developer Portal next to your product.
 
VS2019 SDK Credentials
Get Credentials button on the SDK page


Alternatively, you can find these values by navigating to your product and clicking on Product Settings on the left menu. You will need the Product ID, Sandbox ID, Deployment ID, Client ID, and Client Secret.

Next, let's compile the C# SDK Samples.

  1. Start by opening the Samples.sln file from the C# SDK Samples folder in Visual Studio 2019.
  2. Everything should be good to go here, so hit Ctrl+Shift+B to build all projects.

The C# Samples projects all share the same credentials, which are located in the Common project in Settings.cs.

Setting up our C# solution

Now that we have everything installed and tested, it's time to start setting up our own C# solution.
  1. Open a new instance of Visual Studio 2019 and select Create a new project.
  2. Type "wpf" in the Search for templates (Alt+S) search box at the top and click on the WPF Application [C#] [Windows] [Desktop] entry. Click Next to continue.
  3. Provide a project name (e.g. EOSCSharpSample), Location, and Solution name (defaults to the project name). Click Next to continue.
  4. Select .NET Core 3.1 (Long-term support) as our Target Framework and click Create to complete creating the solution.
  5. Right-click on the solution node in Solution Explorer and click Open Folder in File Explorer. Create a new folder named SDK and copy the SDK\Source subfolder of the downloaded C# SDK here, as we'll need to include these files into our project next.
  6. To include the SDK source and wrapper files we just copied, we're going to modify the Project File. Right-click on the project node in Solution Explorer and click Edit Project File.
  7. Copy the following XML somewhere inside the Project node of the XML, next to the existing PropertyGroup node:

<ItemGroup>
    <Compile Include="..\SDK\Source\Core\**">
        <Link>SDK\Core\%(RecursiveDir)%(Filename)%(Extension)</Link>
    </Compile>
    <Compile Include="..\SDK\Source\Generated\**">
        <Link>SDK\Generated\%(RecursiveDir)%(Filename)%(Extension)</Link>
    </Compile>
</ItemGroup>

 
  1. Next, we'll need to add a conditional compilation symbol to inform the SDK which platform we're targeting. Right-click the project node in Solution Explorer and click Properties.
  2. Click on the Build tab on the left and enter EOS_PLATFORM_WINDOWS_32 (or EOS_PLATFORM_WINDOWS_64 if you prefer) in the Conditional compilation symbols textbox. We'll need to match our Platform target accordingly, so select x86 (or x64) from the drop-down.
  3. Lastly, we need to include the SDK binary into our application. To do so, copy the EOSSDK-Win32-Shipping.dll (or EOSSDK-Win64-Shipping.dll) file from the SDK\Bin subfolder of the downloaded C# SDK directly to your Project in Solution Explorer.
  4. Click on the added DLL in Solution Explorer and change the Copy to Output Directory property to Copy if newer.
  5. Build the solution with Ctrl+Shift+B to ensure everything compiles, or fix any errors.

Brief Model-View-ViewModel explainer

To keep things clear and modular, I'll be using the Model-View-ViewModel architectural pattern to set up the solution and build out each service's functionality. If you've never used MVVM before, don't worry, I'll try to keep it light. Here's a brief summary of the concepts we'll use:
 
  • Commands - Functionality we'll bind to buttons on Views to execute code
  • Converters - Helper functionality to convert one type (e.g. string) to a different type (e.g. boolean) depending on what we want (e.g. convert empty strings to a false boolean value)
  • Helpers - General helper functionality, such as an implementation of INotifyPropertyChanged which will help update the UI when underlying values in the ViewModel change
  • Services - Our main classes for functionality
  • ViewModels - A container for the values and Command instances we'll use in our Views
  • Views - The UI components we'll present to the user

As we'll implement these concepts, their functionality will be clarified.

EOS SDK initialization

To wrap up our solution setup, we'll implement the EOS SDK initialization code, so we can start adding additional EOS service implementations in future articles.

EOS functionality is grouped into Interfaces, which provide a logical grouping of functionality per service. The primary interface in EOS is called the Platform Interface, which provides access to all other interfaces. As such, we must first initialize the SDK and create an instance of the Platform Interface, which we can then use throughout our application.
 
  1. Start by adding a new class to the root of the project called ApplicationSettings.cs. Be sure to drop in your own SDK credentials for each setting.
public class ApplicationSettings
{
    public string ProductId { get; private set; } = "";

    public string ClientId { get; private set; } = "";
    public string ClientSecret { get; private set; } = "";

    public string SandboxId { get; private set; } = "";

    public string DeploymentId { get; private set; } = "";

    public PlatformInterface PlatformInterface { get; set; }

    public void Initialize(Dictionary<string, string> commandLineArgs)
    {
        // Use command line arguments if passed
        ProductId = commandLineArgs.ContainsKey("-productid") ? commandLineArgs.GetValueOrDefault("-productid") : ProductId;
        SandboxId = commandLineArgs.ContainsKey("-sandboxid") ? commandLineArgs.GetValueOrDefault("-sandboxid") : SandboxId;
        DeploymentId = commandLineArgs.ContainsKey("-deploymentid") ? commandLineArgs.GetValueOrDefault("-deploymentid") : DeploymentId;
        ClientId = commandLineArgs.ContainsKey("-clientid") ? commandLineArgs.GetValueOrDefault("-clientid") : ClientId;
        ClientSecret = commandLineArgs.ContainsKey("-clientsecret") ? commandLineArgs.GetValueOrDefault("-clientsecret") : ClientSecret;
    }
}

 
  1. This file will track our SDK credentials, main PlatformInterface instance (which is the core of the SDK, providing access to all other services), and command-line initialization code. If you publish a game to the Epic Games Store, it will pass information to your game via command-line arguments, such as the user ID. We'll continue to use this file going forward to track initialization variables (such as authentication credential type and scopes).
  2. Resolve all unknown types by adding the relevant using statements to the file.
  3. Next, add a new Resource Dictionary (WPF) file named ApplicationResources.xaml to the root of the project. We can leave this blank for now, but we'll use this to add references to Converters we'll use in future articles. Open App.xaml and add this new ResourceDictionary with the following XAML:
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="/ApplicationResources.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

 
  1. Open App.xaml.cs and insert the following code to create a static reference to our ApplicationSettings and parse any command-line arguments that are being passed:

public partial class App : Application
{
    public static ApplicationSettings Settings { get; set; }

    protected override void OnStartup(StartupEventArgs e)
    {
        // Get command line arguments (if any) to overwrite default settings
        var commandLineArgsDict = new Dictionary<string, string>();
        for (int index = 0; index < e.Args.Length; index += 2)
        {
            commandLineArgsDict.Add(e.Args[index], e.Args[index + 1]);
        }

        Settings = new ApplicationSettings();
        Settings.Initialize(commandLineArgsDict);

        base.OnStartup(e);
    }
}

 
  1. Lastly, we'll use MainWindow.xaml.cs to set up and initialize the SDK. Start by adding a timer to simulate game ticks. The EOS SDK uses this timer to trigger callback methods for service calls:

private DispatcherTimer updateTimer;
private const float updateFrequency = 1 / 30f;

 
  1. Add an event handler for the Window Closing event to release the PlatformInterface instance and complete the shutdown process.

public MainWindow()
{
    InitializeComponent();

    Closing += MainWindow_Closing;
}

private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    App.Settings.PlatformInterface.Release();
    App.Settings.PlatformInterface = null;

    _ = PlatformInterface.Shutdown();
}

 
  1. We'll define an InitializeApplication() function, which we'll call when the MainWindow gets instantiated, so we can set up the SDK. 

private void InitializeApplication()
{
    var initializeOptions = new InitializeOptions()
    {
        ProductName = "EOSCSharpSample",
        ProductVersion = "1.0.0"
    };

    var result = PlatformInterface.Initialize(initializeOptions);
    Debug.WriteLine($"Initialize: {result}");

    _ = LoggingInterface.SetLogLevel(LogCategory.AllCategories, LogLevel.Info);
    _ = LoggingInterface.SetCallback((LogMessage message) => Debug.WriteLine($"[{message.Level}] {message.Category} - {message.Message}"));

    var options = new Options()
    {
        ProductId = App.Settings.ProductId,
        SandboxId = App.Settings.SandboxId,
        ClientCredentials = new ClientCredentials()
        {
            ClientId = App.Settings.ClientId,
            ClientSecret = App.Settings.ClientSecret
        },
        DeploymentId = App.Settings.DeploymentId,
        Flags = PlatformFlags.DisableOverlay,
        IsServer = false
    };

    PlatformInterface platformInterface = PlatformInterface.Create(options);

    if (platformInterface == null)
    {
        Debug.WriteLine($"Failed to create platform. Ensure the relevant settings are set.");
    }

    App.Settings.PlatformInterface = platformInterface;

    updateTimer = new DispatcherTimer(DispatcherPriority.Render)
    {
        Interval = new TimeSpan(0, 0, 0, 0, (int)(updateFrequency * 1000))
    };

    updateTimer.Tick += (sender, e) => Update(updateFrequency);
    updateTimer.Start();
}


Let's unpack that a bit: 
  • In general, EOS service calls consist of instantiating an Options class, which will get passed into the function you're calling. In this case, we're instantiating InitializeOptions and passing it to the Initialize() function of PlatformInterface.
  • This particular function returns a result immediately, but often a service call will take a callback function as a parameter, which will get triggered when the function completes. We'll see an example of that in the next article, where we look at Authentication.
  • Next, we're using the Logging Interface to define how we want to receive our logs. In our sample application, we'll have all log messages be output to the Debug output window, but this is where you could also set up writing logs to a file, or a cloud service if you choose to do so.
  • Then we repeat the same Options -> call pattern with PlatformInterface.Create to create the PlatformInterface and store the instance in App.Settings for later retrieval throughout the app.
  • Lastly, we set up our timer with an interval of updateFrequency (in this case 1/30), set the tick event handler, and start it. The general guidance here is to set this interval to equal your game tick rate to ensure responsiveness in API callbacks. Note that the SDK is not thread safe and all SDK calls should be made from the same thread that initialized it.
 
  1. The timer tick event handler calls the PlatformInterface Tick method to ensure any asynchronous callbacks are called successfully.

private void Update(float updateFrequency)
{
    App.Settings.PlatformInterface?.Tick();
}

 
  1. Finally, add a call to InitializeApplication() in the MainWindow constructor

At this point, you should be able to compile and run your application, and in the Debug output window you should see a few log messages indicating successful initialization:

Initialize: Success
[Info] LogEOSOverlay - Overlay will not load, because it was explicitly disabled when creating the platform
[Info] LogEOSAntiCheat - [AntiCheatClient] Anti-cheat client not available. Verify that the game was started with the correct launcher if you intend to use it.
[Info] LogEOS - Updating Platform SDK Config, Time: 0.368525
[Info] LogEOS - SDK Config Platform Update Request Successful, Time: 0.821195
[Info] LogEOSAnalytics - Start Session (User: ...)
[Warning] LogEOSAnalytics - EOS SDK Analytics disabled for route [1].
[Info] LogEOS - Updating Product SDK Config, Time: 0.866974
[Info] LogEOSAnalytics - Start Session (User: ...)
[Info] LogEOS - SDK Config Product Update Request Successful, Time: 1.350656
[Info] LogEOS - SDK Config Data - Watermark: 82749226
[Info] LogEOS - ScheduleNextSDKConfigDataUpdate - Time: 1.350656, Update Interval: 325.699646

Building out our MVVM architecture

Before we wrap up this article, we need to build out our MVVM architecture a bit more so we can easily insert additional functionality in future articles:
  1. Start by creating a folder called Helpers in the root of our project.
  2. Here we'll create two Helper classes, which will assist in our MVVM implementation. I'm not going to cover their functionality, but this can easily be found in other articles. Create a new class called BindableBase.cs:

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Equals(storage, value)) return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var eventHandler = PropertyChanged;
        if (eventHandler != null)
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
    }
}

 
  1. Next, create a class called CommandBase.cs:

public class CommandBase : ICommand
{
    public virtual bool CanExecute(object parameter)
    {
        throw new NotImplementedException();
    }

    public virtual void Execute(object parameter)
    {
        throw new NotImplementedException();
    }

    public event EventHandler CanExecuteChanged;
    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, new EventArgs());
    }
}

 
  1. With these 2 classes created, create a folder called ViewModels in the root of our project. This is where we'll store our ViewModels and a ViewModelLocator class to reference them. Start by creating a class called MainViewModel.cs:

public class MainViewModel : BindableBase
{
    private string _statusBarText;
    public string StatusBarText
    {
        get { return _statusBarText; }
        set { SetProperty(ref _statusBarText, value); }
    }

    public MainViewModel()
    {
    }
}

 
  1. Next, create a class called ViewModelLocator.cs:

public static class ViewModelLocator
{
    private static MainViewModel _main;
    public static MainViewModel Main
    {
        get { return _main ??= new MainViewModel(); }
    }
}

 
  1. Next, we'll create a few Converters that will assist in some UI automation. Create a folder called Converters in the root of our project and add a class called StringToBooleanConverters.cs:

[Bindable(BindableSupport.Default)]
public class StringToBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null || !(value is string))
        {
            return false;
        }

        string stringValue = value as string;

        return !string.IsNullOrWhiteSpace(stringValue.Trim());
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

 
  1. And add another class called StringToVisibilityConverter.cs:

[Bindable(BindableSupport.Default)]
public class StringToVisibilityConverter : IValueConverter
{

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null || !(value is string))
        {
            return Visibility.Collapsed;
        }

        string stringValue = value as string;

        return string.IsNullOrWhiteSpace(stringValue.Trim()) ? Visibility.Collapsed : (object)Visibility.Visible;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

 
  1. Lastly, open ApplicationResources.xaml in the root of our project and replace it with the following XAML. Note that if your project is not named EOSCSharpSample, you'll have to change the namespace next to xmlns:c="..."

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:c="clr-namespace:EOSCSharpSample.Converters">

    <c:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
    <c:StringToBooleanConverter x:Key="StringToBooleanConverter" />

</ResourceDictionary>

Get the code

You can find the code for this article below. Make sure you follow step 5 and 9 of the Setting up our C# solution section to add the SDK to the solution, and you edit ApplicationSettings.cs to include your SDK credentials.

In the next article, we'll start using Account Services to log in using an Epic Account. Be sure to check out the series reference of the initial article for a quick overview of all articles in the series.

    We succeed when you succeed

    Epic believes in an open, integrated games community. By offering our online services to everyone for free, we aim to empower more developers to serve their own player communities.