Integration Of Your Game in Steam: Working With the Lobby in Steamworks.NET

GemesUtra

Video Games Guides, Reviews & News

Integration Of Your Game in Steam: Working With the Lobby in Steamworks.NET

Few gamers have not heard about Steam. The first appearance of the grounds have as much as 2002, it major publishers can safely distribute your game.

A dozen years later, Steam Greenlight appeared, which made it possible to get on the site not only for large studios, but also for ordinary indie developers. Users chose which games they wanted to see on the site. But because of the mass of second-rate games, such a system had to be closed. In place of Greenlight came Direct. According to the developers, such a system should make the publishing process orderly, transparent and accessible to new developers from all over the world.

For almost two decades, Steam has ceased to be just a platform for digital distribution. It has an internal economy, achievements, collectible cards, inventory. All this is necessary to increase the player’s involvement. Naturally, it was necessary to allow developers to somehow integrate these components of Steam in their games. Steamworks was created for this purpose.

Steam’s history as a multi-user platform began with CS 1.6. Multiplayer has always been one of the key aspects of the game. The platform gives players the opportunity to communicate with each other over a peer-to-peer network (P2P), or use dedicated game servers. For the first case, of course, you need matchmaking – the process of combining players in a game session. Player recruitment takes place in the lobby, where players can discuss various game aspects, choose characters and a map. Steamworks provides a comprehensive API for working with matchmaking.

steam-img

Note: the Original Steamworks runs in C++. In the same article we will talk about C# — Steamworks.NET This is a full-fledged wrapper of the official Steamworks. Steam has full documentation for matchmaking.

Installation
(for Unity3D)

  1. Download from the repository Steamworks.NET the current version of the SDK.
  2. Move all content to the Assets folder.
  3. Run the project in Unity3D. After starting the project, a file will be created in the root of the project
    steam_appid.txt. This file should store your Steam app ID. If you don’t have one yet, you can use the standard ID 480. It belongs to the game Spacewar.
  4. Restart Unity3D so that the file changes take effect.
  5. Note the presence of the file SteamManager.cs. It performs several extremely important functions. If there is no file, it can always be found in the repository.

Introduction

Callback and CallResult play a key role in Steamworks. Callbacks allow the game to work asynchronously with Steam.

Callback is called at any events in Steam. This can be the event of receiving a message in the chat, changing the list of players in the lobby, or even opening the game overlay. Consider the following code taken from the wiki Steamworks.NET:

public class SteamScript : MonoBehaviour {
	Callback m_GameOverlayActivated;

	private void OnEnable() {
		if (SteamManager.Initialized)
			m_GameOverlayActivated = Callback.Create(OnGameOverlayActivated);
	}

	private void OnGameOverlayActivated(GameOverlayActivated_t pCallback) {
		if(pCallback.m_bActive != 0)
			Debug.Log("The Steam overlay is open");
		else 
			Debug.Log("Steam overlay closed");
	}
}

First, you need to create a Callback instance. In this case, it is the opening/closing event of the overlay. Callback should be initialized by binding a function to it. This should only be done after making sure that Steam is already initialized: SteamManager.Initialized. The best way to do this is to use the OnEnable () method, which is called immediately at the start of the game.

A variable containing the result of the event will be passed to the function. Each callback type has its own variable type.

 

CallResult is very similar to Callback. The difference is that CallResult is the result of calling a specific method. This can be, for example, the result of creating a lobby or connecting to it. Consider the code from the wiki:

public class SteamScript : MonoBehaviour {
	private CallResult m_NumberOfCurrentPlayers;

	private void OnEnable() {
		if (SteamManager.Initialized)
			m_NumberOfCurrentPlayers = CallResult.Create(OnNumberOfCurrentPlayers);
	}

	private void Update() {
		if(Input.GetKeyDown(KeyCode.Space)) {
			SteamAPICall_t handle = SteamUserStats.GetNumberOfCurrentPlayers();
			m_NumberOfCurrentPlayers.Set(handle);
			Debug.Log("Method called
GetNumberOfCurrentPlayers()"
); } } private void OnNumberOfCurrentPlayers(NumberOfCurrentPlayers_t pCallback, bool bIOFailure) if (pCallback.m_bSuccess != 1 || bIOFailure) { Debug.Log("An error occurred during processing
NumberOfCurrentPlayers."
);
else Debug.Log("Number of players in the game
: "
+ pCallback.m_cPlayers)
; } }

As with Callback, you first need to create an instance of CallResult and initialize it. In the Update() method, a check is made for pressing a space. By clicking, a request will be sent to get the number of players. As in the previous case, after receiving the response, the specified method is called and the result is passed to it.

Note that when working with CallResult, the method signature will always be bool bIOFailure.

For Callback and CallResult to work, you need to call the method cyclically SteamAPI.RunCallbacks().

Preparation

To work with matchmaking you will need some structures:

struct LobbyMetaData
{
    public string m_Key;
    public string m_Value;
}

struct LobbyMembers
{
    public CSteamID m_SteamID;
    public LobbyMetaData[] m_Data;
}

struct Lobby
{
    public CSteamID m_SteamID;
    public CSteamID m_Owner;
    public LobbyMembers[] m_Members;
    public int m_MemberLimit;
    public LobbyMetaData[] m_Data;
}

Each lobby has its own metadata: the name of the map or the game mode. There is no template data — everything is left to the developer. To work with metadata, you will need the Lobby MetaData structure. It is a standard key-value pair.

In the lobby each player is a structure LobbyMembers, the main property of which is  m_SteamID — the unique ID of the Steam user.

The  Lobby  structure describes the lobby itself, or rather the most necessary properties, such as:

  • ➤unique lobby ID;
  • ➤lobby owner ID;
  • ➤list of players in the lobby;
  • ➤maximum number of players in the lobby;
  • ➤metadata the lobby.

You will also need some instances of Callbacks and CallResult, namely:

CallResult m_LobbyEnterCallResult; // При входе в лобби
CallResult m_LobbyMatchListCallResult; // При получении списка лобби
CallResult m_LobbyCreatedCallResult; // При создании лобби
Callback m_LobbyChatMsgCallResult; // При получении сообщения в лобби
Callback m_LobbyChatUpdateCallResult; // При изменении списка игроков в лобби (когда какой-либо игрок входит в лобби или выходит)
Callback m_LobbyDataUpdateCallResult; // При обновлении мета-данных лобби

And as expected, you should initialize all callbacks and create appropriate methods for them:

void OnEnable()
{
    if (!SteamManager.Initialized)
        return;

    m_LobbyEnterCallResult = CallResult.Create(OnLobbyEnter);
    m_LobbyMatchListCallResult = CallResult.Create(OnLobbyMatchList);
    m_LobbyCreatedCallResult = CallResult.Create(OnLobbyCreated);
    m_LobbyChatMsgCallResult = Callback.Create(OnLobbyChatMsg);
    m_LobbyChatUpdateCallResult = Callback.Create(OnLobbyChatUpdate);      
    m_LobbyDataUpdateCallResult = Callback.Create(OnLobbyDataUpdate);
}

void OnLobbyEnter(LobbyEnter_t pCallback, bool bIOFailure)
{
    // At the entrance to the lobby...
}

void OnLobbyMatchList(LobbyMatchList_t pCallback, bool bIOFailure)
{
  // When receiving the list of lobbies...
}

void OnLobbyCreated(LobbyCreated_t pCallback, bool bIOFailure)
{
    // When creating a lobby...
}

void OnLobbyChatMsg(LobbyChatMsg_t pCallback)
{
    // When you receive a message in the lobby...
}

void OnLobbyChatUpdate(LobbyChatUpdate_t pCallback)
{
    // When changing the list of players in the lobby...
}

void OnLobbyDataUpdate(LobbyDataUpdate_t pCallback)
{
    // When you update metadata in the lobby...
}

Getting a list of lobbies

To get a list of existing lobbies, use:

m_LobbyMatchListCallResult.Set(SteamMatchmaking.RequestLobbyList());

After receiving the response, the OnLobbyMatchList method is called. Only one number is passed to the method — the number of lobbies. It can be taken from a variable pCallback.m_nLobbiesMatching.

Attention: Steamworks can return no more than 50 lobbies in the list.

After getting a list of lobbies, it would be nice to display them. Iterating through the list of lobbies will look like this:

for (int i = 0; i < pCallback.m_nLobbiesMatching; i++)       
    RenderLobby(SteamMatchmaking.GetLobbyByIndex(i));

You will need to create some method for displaying the lobby list (RenderLobby), which will accept the lobby ID:

Lobby lobby = new Lobby();
lobby.m_SteamID = steamIDLobby; // ID, which was passed to the method
lobby.m_Owner = SteamMatchmaking.GetLobbyOwner(steamIDLobby);
lobby.m_Members = new LobbyMembers[SteamMatchmaking.GetNumLobbyMembers(steamIDLobby)];
lobby.m_MemberLimit = SteamMatchmaking.GetLobbyMemberLimit(steamIDLobby);     

int DataCount = SteamMatchmaking.GetLobbyDataCount(steamIDLobby);

lobby.m_Data = new LobbyMetaData[DataCount];
for (int i = 0; i < DataCount; i++) // Getting all the lobby metadata
{
    bool lobbyDataRet = SteamMatchmaking.GetLobbyDataByIndex(steamIDLobby, i, out lobby.m_Data[i].m_Key, 
        Constants.k_nMaxLobbyKeyLength, out lobby.m_Data[i].m_Value, Constants.k_cubChatMetadataMax);
            
    if (!lobbyDataRet){
        Debug.LogError("Error retrieving lobby metadata");
        continue;
    }
}
//
// Displaying the lobby in the list...
//

Then you need to allow the user to choose the lobby to which he wants to connect, or create your own.

Filter the lobby list

Steamworks allows you to filter the returned list by certain categories.

Attention the filter Must be set before calling RequestLobbyList().

First, you can specify the maximum number of returned lobbies. The smaller the number — the faster the result will be processed. This can be done by the function SteamMatchmaking.AddRequestLobbyListResultCountFilter(max_count);.

Then there are several types of filters (all of them are in the class
SteamMatchmaking):

  • ➤AddRequestLobbyListDistanceFilter — specifies the distance within which to search for a lobby (based on the user’s IP).
    Takes ELobbyDistanceFilter.
  • ➤AddRequestLobbyListFilterSlotsAvailable — leaves only those lobbies where the specified number of free slots is available.
  • ➤AddRequestLobbyListNearValueFilter — sorts the lobby by the distance of the value from the specified one. You can specify several such filters. The first will have the most influence on sorting, the last-the least.
  • ➤AddRequestLobbyListNumericalFilter — specifies the numeric comparison type.
  • ➤AddRequestLobbyListStringFilter — specifies the string type of the comparison.

The last three filters compare / sort lobbies by their metadata.

For example, after creating a lobby, you choose a location. The name of the location will need to be saved in the metadata of the lobby under the key map_name. Other users will be able to filter the list of lobbies that have the metadata value
map_name will be the ones they put up. This way the player will be able to find a lobby with the location that he wants.

Connecting to the lobby

To join an existing lobby:

m_LobbyEnterCallResult.Set(SteamMatchmaking.JoinLobby(LobbyID));

After successfully connecting to the lobby, the OnLobbyEnter method will be called. All other members of the lobby will have the OnLobbyChatUpdate method called.

Creating your own lobby

To create your own lobby, use the method:

m_LobbyCreatedCallResult.Set(SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypePublic, 4));

The CreateLobby method takes two parameters. The first is the type of lobby visibility (by invitation/for friends/open); the second is the maximum number of players. The lobby can have up to 250 players, although in practice-from 2 to 5.

After successfully creating the lobby, the OnLobbyCreated method will be called.

Stay in the lobby

Most likely, in the lobby you will need to display a list of players and chat. This will require some methods. For example, to get a Sprite containing a user’s avatar, use the method:

public static Sprite GetUserAvatar(CSteamID ID)
{
    Texture2D original = null;
    uint width, height;

    int image = SteamFriends.GetLargeFriendAvatar(ID); 

    bool IsValid = SteamUtils.GetImageSize(image, out width, out height);

    if (IsValid)
    {
        byte[] data = new byte[width * height * 4];

        IsValid = SteamUtils.GetImageRGBA(image, data, (int)(width * height * 4));
        if (IsValid)
        {
            original = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false, true);
            original.LoadRawTextureData(data);
            original.Apply();
        }
    }

    Texture2D flipped = new Texture2D((int)width, (int)height);

    int x = (int) width, y = (int) height;

    for (int i = 0; i < x; i++)
        for (int j = 0; j < y; j++)
            flipped.SetPixel(j, x - i - 1, original.GetPixel(j, i));

    flipped.Apply();

    return Sprite.Create(flipped, new Rect(0f, 0f, original.width, original.height), Vector2.zero);
}

In this case, the resolution of the avatar will be 128×128 pixels.

To get your own Steam ID, use SteamUser.Get the Steam ID(). To get your name — SteamFriends.GetPersonaName(). If you need to get the name of another user — SteamFriends.GetFriendPersonaName(PlayerID).

Sending messages to the lobby

Steamworks allows you to exchange information in the lobby. This can be a notification about the readiness of a player, a change of character or a banal receipt of messages in the chat. In any case you will need the following method:

void SendData(string data)
{
    byte[] bytes = System.Text.Encoding.Default.GetBytes(data);
    SteamMatchmaking.SendLobbyChatMsg(current_lobby_id, bytes, bytes.Length + 1);
}

Note that you must update the  current_lobby_id  variable yourself when creating your own lobby or connecting to an existing one.

After receiving the message, all users (including the sender) will call the OnLobbyChatMsg method.

A good solution would be to create a data object that will have a specific type (chat message, changing the player’s readiness, etc.). To send data, you will need to serialize it to a string (for example, JSON) and send it to everyone else via  SendData. When you receive such a message, you will need to deserialize the message into an object, determine its type, and process it. Then OnLobbyChatMsg  will start something like this:

void OnLobbyChatMsg(LobbyChatMsg_t pCallback)
{
    CSteamID SteamIDUser; // ID отправителя

    byte[] Data = new byte[4096]; // Максимальный размер сообщения
    EChatEntryType ChatEntryType;
    int ret = SteamMatchmaking.GetLobbyChatEntry((CSteamID)pCallback.m_ulSteamIDLobby, (int)pCallback.m_iChatID, out SteamIDUser, Data, Data.Length, out ChatEntryType);

    string data = System.Text.Encoding.Default.GetString(Data);
    //
    // Further processing of data...
    //
}

Changing lobby metadata

As mentioned earlier, metadata is needed to store any game information about the lobby: the name of the map, the game mode, the minimum level, and so on. only the owner of the lobby can Change the meta data. You can use this method to quickly check the ownership of a lobby:

public bool IsLobbyOwner() => SteamUser.GetSteamID() == current_lobby_owner;

This method is used to create or modify metadata:

SteamMatchmaking.SetLobbyData(current_lobby_id, key, value);

As you can see from the code, the SetLobbyData method works according to the standard key-value scheme. For all other members of the lobby, there is a separate similar method SetLobbyMemberData.

As soon as the data is sent, the OnLobbyDataUpdate method is called for all clients. New players who have just entered the lobby will receive new metadata values immediately.

Note. There is a slight delay before sending the data. Several consecutive metadata changes will be combined and sent in a single batch.

To delete metadata, use:

SteamMatchmaking.DeleteLobbyData(current_lobby_id, key);
 
Most Viewed Posts
  • TFT Patch 10.20 Patch Notes: Balance Changes, Traits, Release, more (5)
    TFT Patch 10.20 is here, and so are the first major balance changes for TFT Fates. Riot have targeted two traits in particular for nerfs, while a handful of underpowered units will be getting a leg-up. Here’s everything you need to know. Fates has now been on live servers for two weeks, and Riot are […]
  • Among Us – Best Mouse & Keyboard Bindings (6)
    Keybindings to make actions a little quicker Unfortunately, you cannot change keybinds. The best you can do is to set up a script with AutoHotkey. AZERTY users: If you enable an English keyboard in your Windows settings, you can switch to that and it will theoretically let you use ZQSD. Bindings WASD and ARROW KEYS - Movement TAB - […]
  • How to Find the Ghost in Asylum or High School Faster in Phasmophobia (6)
    These are the steps on how I do it in-game during Asylum / High school run. Guide to Find the Ghost Faster Introduction Mainly this is gonna be a small guide, but lets run with it. This is the steps that I run on either asylum or highschool for the best experience or a way […]
  • All Cheat Codes in Serious Sam 4 & How to Enable (7)
    For Serious Sam 4 players who are looking for cheats, this is a simple guide detailing how to use cheat codes, the cheat codes themselves and their effects within the game. How to enable cheats! Cheats can be enabled by simply loading into the game and typing the codes in the console which is accessed by […]
  • FIFA 21 Celebrations: New Controls Guide (7)
    With FIFA 21 on the horizon, attention has turned to the celebrations you’ll be able to gloat over opposing players with. So, here’s all the new celebrations in EA’s reveals as well as their controls.  To some FIFA players, celebrating might seem like a small part of the game – or even a time-wasting nuisance. […]

Leave a Reply

Your email address will not be published. Required fields are marked *