Announcement

Collapse
No announcement yet.

Implementing ServerBrowser Functionality with Steam

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    Implementing ServerBrowser Functionality with Steam

    Hi all,

    as promised in a different thread, here's a little tutorial on how to implement some basic functionality for a Server-Browser using Steam.
    Please note that it's already quite late at my place and i'm back from a long D3 Session, so some stuff in this tutorial may sound a bit weird

    What i WONT explain here:
    - How to build the UI for the ServerBrowser (i'm a coder, not a UI-Designer :P )
    - Where to put the Classes (if you really want to integrate steam, you should know what to do here)


    First, the necessary configuration changes:

    In your DefaultEngine.ini:
    Set GameDir to something that would be unique to your game. This will differ your game from all the other UDK-Servers out there.
    Also, set the NetworkDevice to Steamworks.
    Code:
    [OnlineSubsystemSteamworks.OnlineSubsystemSteamworks]
    GameDir=fbmtest
    
    [Core.System]
    NetworkDevice=OnlineSubsystemSteamworks.IpNetDriverSteamworks
    If you want some debugging Output in the logfiles, you can open UDKEngine.ini and remove
    Code:
    Suppress=DevOnline
    Suppress=DevOnlineGame
    But note that with the next Refresh of the Configfiles, these changes will be automatically overwritten.


    So, your configuration is now setup.

    Lets get to code something.

    Step 1: Creating the needed classes

    What do we need? Well, we need to
    - tell UDK what Settings we want to have for our game and
    - what the SearchSettings are that we want to use

    GameSettings:
    This class will hold stuff like the ServerName, MapName etc.
    The interesting stuff is
    - the steamserverid, since this is our server-connection to steam. This one will be filled automatically by the OnlineSubsystem
    - the inclusion of some constants (which makes it easier to access the properties)
    - the BuildURL - function (this one will be called later to append all properties you may have set to the Url for the actual servertravel)

    For an advanced example for GameSettings you can have a look into the UT-GameSettings-Classes.

    Code:
     class MyGameSettings extends UDKGameSettingsCommon;
    
    `include(MyOnlineConstants.uci)
    
    /** The UID of the steam game server, for use with steam sockets */
    var databinding string SteamServerId;
    
    /**
     * Builds a URL string out of the properties/contexts and databindings of this object.
     */
    function BuildURL(out string OutURL) {
    	local int SettingIdx;
    	local name PropertyName;
    
    	OutURL = "";
    
    	// Append properties marked with the databinding keyword to the URL
    	AppendDataBindingsToURL(OutURL);
    
    	// add all properties
    	for (SettingIdx = 0; SettingIdx < Properties.length; SettingIdx++) {
    		PropertyName = GetPropertyName(Properties[SettingIdx].PropertyId);
    		if (PropertyName != '') {
    			switch(Properties[SettingIdx].PropertyId) {
    				default:
    					OutURL $= "?" $ PropertyName $ "=" $ GetPropertyAsString(Properties[SettingIdx].PropertyId);
    					break;
    			}
    		}
    	}
    }
    function setServerName(string serverName) {
    	SetStringProperty(PROPERTY_MYSERVERNAME, serverName);
    }
    
    function string getServerName() {
    	return GetPropertyAsString(PROPERTY_MYSERVERNAME);
    }
    
    DefaultProperties
    {
    	// Properties and their mappings
    	Properties(0)=(PropertyId=PROPERTY_MYSERVERNAME,Data=(Type=SDT_String),AdvertisementType=ODAT_QoS)
    	PropertyMappings(0)=(Id=PROPERTY_MYSERVERNAME,Name="MyServerName")
    }
    The MyOnlineConstants.uci:
    Code:
    const PROPERTY_MYSERVERNAME                        = 0x40000001;
    Pretty straightforward.
    If you want to know what the AdvertisementType is, and what other Types apart from "String" you can use:
    AdvertisementType
    Type

    Now, lets go on to the SearchSettings:
    Code:
    class MyOnlineSearchSettings extends OnlineGameSearch;
    
    defaultproperties
    {
    	// Override this with your game specific class so that metadata can properly
    	// expose the game information to the UI
    	GameSettingsClass=class'MyGameSettings'
    
    }
    Wow, what a bummer. Nevertheless this is a nice one since it will tell the OnlineSearch which GameSettings class it will return. Otherwise you would always just get the normal OnlineGameSettings class without your steamserverid.

    Step 2: Hosting a game through Steam

    Here come the methods you will need for Hosting a Game over Steam. Please not that it depends on programming style, where and how you implement these (also applies for Searching and Joining).
    You could, for example, directly implement this in your HUD.

    Code:
    var OnlineSubsystem OnlineSub;
    var OnlineGameInterface GameInterface;
    var MyGameSettings currentGameSettings;
    
    /**
     * Initializes the variables for the OnlineSubSystem
     */
    function InitOnlineSubSystem() {
    	// Figure out if we have an online subsystem registered
    	OnlineSub = class'GameEngine'.static.GetOnlineSubsystem();
    
    	if (OnlineSub == None) {
    		`Log("CreateOnlineGame - No OnlineSubSystem found.");
    		return;
    	}
    
    	GameInterface = OnlineSub.GameInterface;
    
    	if (GameInterface == None) {
    		`Log("CreateOnlineGame - No GameInterface found.");
    	}
    }
    
    /**
     * Creates the OnlineGame with the Settings we want
     */
    function CreateOnlineGame() {
    	// Create the desired GameSettings
    	currentGameSettings = new class'MyGameSettings';
    	currentGameSettings.bShouldAdvertise = true;
    	currentGameSettings.NumPublicConnections = 32;
    	currentGameSettings.NumPrivateConnections = 32;
    	currentGameSettings.NumOpenPrivateConnections = 32;
    	currentGameSettings.NumOpenPublicConnections = 32;
    	currentGameSettings.bIsLanMatch = false;
    	currentGameSettings.setServerName("My Test Server on Steam");
    
    	// Create the online game
    	// First, set the delegate thats called when the game was created (cause this is async)
    	GameInterface.AddCreateOnlineGameCompleteDelegate(OnGameCreated);
    
    	// Try to create the game. If it fails, clear the delegate
    	// Note: the playerControllerId == 0 is the default and noone seems to know what it actually does...
    	if (GameInterface.CreateOnlineGame(class'UIInteraction'.static.GetPlayerControllerId(0), 'Game', currentGameSettings) == FALSE ) {
    		GameInterface.ClearCreateOnlineGameCompleteDelegate(OnGameCreated);
    		`Log("CreateOnlineGame - Failed to create online game.");
    	}
    }
    
    /**
     * Delegate that gets called when the OnlineGame has been created.
     * Actually sends the player to the game
     */
    function OnGameCreated(name SessionName, bool bWasSuccessful) {
    	local string TravelURL;
    	local Engine Eng;
    	local PlayerController PC;
    
    	// Clear the delegate we set.
    	GameInterface.ClearCreateOnlineGameCompleteDelegate(OnGameCreated);
    
    	if (bWasSuccessful) {
    		Eng = class'Engine'.static.GetEngine();
    		PC = Eng.GamePlayers[0].Actor;
    
    		// Creation was successful, so send the player on the host to the level
    		// Build the URL
    		currentGameSettings.BuildURL(TravelURL);
    
    		TravelURL = "open " 
    			$ "MyTestMap"
    			$ "?game=MyGameMode"
    			$ TravelURL $ "?listen?steamsockets";
    		// Do the server travel.
    		PC.ConsoleCommand(TravelURL);
    	} else {
    		`Log("OnGameCreated: Creation of OnlineGame failed!");
    	}
    }
    Thats a bit more now. So lets get into detail:
    InitOnlineSubSystem just initializes the class-variables, so we dont have to do the checks for the onlinesubsystem all the time.

    CreateOnlineGame is a bit more interesting.

    The GameSettings:
    currentGameSettings.bShouldAdvertise = true -> This tells the OnlineSubSystem that yes, we want to advertise this server through steam

    currentGameSettings.setServerName("My Test Server on Steam") -> This is the Property we have set in our own gamesettings...nothing special

    GameInterface.AddCreateOnlineGameCompleteDelegate( OnGameCreated) -> This one is interesting. If you think about it, Advertising your game on steam is a time-consuming process. You dont want to halt your
    UI completely while this is in progress, cause depending on your network, this may take longer (usually not more than half a second, but you never know ). So, the Parameter "OnGameCreated" is a function-name in the same class
    that will get called when the creation of the online game was finished.

    GameInterface.CreateOnlineGame(class'UIInteraction '.static.GetPlayerControllerId(0), 'Game', currentGameSettings) is the actual creation of the onlinegame.

    OnGameCreated has to have the parameters as shown, or it wont get called by the onlineSubSystem.
    The ClearCreateOnlineGameCompleteDelegate is called, so we dont get called again by accident.

    If the creation of the OnlineGame was successfull, we build the URL for the gamesettings and just do an "open" command for it.
    "?listen" is important cause we are hosting the game, so we probably want a listen server.
    "?steamsockets" is important, so we can actually connect through steam and the steamserverid.

    Please note: I will shoot the first one that asks about Errors cause of MyTestMap and MyGameMode here since if you want to integrate Steam you should actually know what this is supposed to be


    Step 3: Searching for games

    This one is quite easy now:

    Code:
    var MyOnlineSearchSettings SearchSetting;
    var array<OnlineGameSearchResult> searchResults;
    
    /**
     * Searches for Online games.
     */
    function SearchOnlineGames() {
    	SearchSetting = new class'MyOnlineSearchSettings';
    	SearchSetting.bIsLanQuery = false;
    	SearchSetting.MaxSearchResults = 50;
    	// Cancel the Search first...cause there may be a former search still in progress
    	GameInterface.CancelFindOnlineGames();
    	GameInterface.AddFindOnlineGamesCompleteDelegate(OnServerQueryComplete);
    	GameInterface.FindOnlineGames(class'UIInteraction'.static.GetPlayerControllerId(0), SearchSetting);
    }
    
    /**
     * Delegate that gets called when the ServerSearch is finished
     */
    function OnServerQueryComplete(bool bWasSuccessful) {
    	local int i;
    	local MyGameSettings gs;
    
    	searchResults = SearchSetting.Results;
    	if (bWasSuccessful) {
    		`Log("success");
    		for (i = 0; i < SearchSetting.Results.Length; i++) {
    			gs = MyGameSettings(SearchSetting.Results[i].GameSettings);
    			// Here you can do something with the GameSettings for each result that was found
    			// gs.getServerName();
    		}
    	} else {
    		`Log("No Results!!!!!");
    	}
    
    	GameInterface.ClearFindOnlineGamesCompleteDelegate(OnServerQueryComplete);
    }
    First, you create a new MyOnlineSearchSettings instance and set parameters like maxSearchResults. Of course you can extend the SearchSettings as you wish.
    Then we again assign a delegate function to be called when the action has finished and finally start the search.


    Step 4: Joining a found game

    This is more or less similar to hosting a game:

    Code:
    /**
     * Joins the game with given index from the searchResults
     */
    public function JoinOnlineGame(int gameIdx) {
    	`Log("going to game");
    	// Set the delegate for notification
    	GameInterface.AddJoinOnlineGameCompleteDelegate(OnJoinGameComplete);
    	GameInterface.JoinOnlineGame(0, 'Game', SearchSetting.Results[gameIdx]);
    }
    
    /**
     * Delegate that gets called when joinGame completes.
     */
    private function OnJoinGameComplete(name SessionName, bool bSuccessful) {
    	local MyGameSettings  myGameSettings;
    	local string travelURL;
    	local Engine Eng;
    	local PlayerController PC;
    	
    	if (bSuccessful == false) {
    		`log("Join Game failed!");
    		return;
    	}
    	
    	Eng = class'Engine'.static.GetEngine();
    	PC = Eng.GamePlayers[0].Actor;
    
    	class'GameEngine'.static.GetOnlineSubsystem().GameInterface.GetResolvedConnectString('Game', travelURL);
    
    	myGameSettings = MyGameSettings(OnlineGameInterfaceImpl(class'GameEngine'.static.GetOnlineSubsystem().GameInterface).GameSettings);
    
    	if (myGameSettings != none && myGameSettings.SteamServerId != "") {
    		PC.ConsoleCommand("open "$"steam."$myGameSettings.SteamServerId);
    	} else {
    		PC.ConsoleCommand("open "@travelURL);
    	}
    }
    So what are we doing here?

    JoinOnlineGame simply gets the index of the desired Game inside the Results-Array, so we can give that to the JoinOnlineGame-Function.
    When the action completes, OnJoinGameComplete is called. The main thing to take care of is, what to do if there's no steamserverid available (since the OnlineSubSystem is generic and steam is just one of the implementations)?
    "class'GameEngine'.static.GetOnlineSubsystem().Gam eInterface.GetResolvedConnectString('Game', travelURL);" resolves the IP-Address of the Server, so we can use that one in case the steamserverid is null.


    This should be enough to get you started. If you still have any questions, feel free to ask in this Thread; BUT, first try one of these:

    Q: My server doesnt seem to get advertised / The search is not finishing or doesnt give any results
    A: Maybe you got to open some ports on your firewall:
    - UDK-Port: 7777
    - Steam-Ports: 27015 - 27016

    Q: I have more questions to the Steam OnlineSubsystem / How can i do XYZ with the OnlineSubSystem
    A: Try to read through this one: http://udn.epicgames.com/Three/Onlin...teamworks.html, apart from that the UT-Sources are a nice example.

    Thanks to schizoslayer for the following one
    Q: After joining the Server there is no Friend-Info / The Session is not correctly filled.
    A: StartMatch() has to be called in your GameInfo - Class (which in turn calls StartOnlineGame()), which is responsible for filling in your friend info to enable join/invite stuff.


    Regards,
    Indy

    #2
    Thank you very muck man !!

    Comment


      #3
      Awesome stuff indygoof, really glad to see this tutorial written!

      Comment


        #4
        awesome stuff! thanks a bunch man
        also that was very quick, just yesterday we were asking you for it

        Comment


          #5
          Great!, I was just looking for this info, while I've discovered most of what you posted about the join and host menu, I still can't find my local game, this ought to clear it up...

          Thanks man!.. The UDK needs more advanced topic tutorials!..

          Comment


            #6
            Hey,
            one more thing: For some reason, even if OnServerQueryComplete is called by the onlinesubsystem, it tells me that the search is still in progress. So for now, it's definitely needed to cancel the former search before starting a new one.
            I will post an update as soon as i know why this happens. Or maybe someone else of the professionals in this forum knows

            And...thx for the positive Responses!
            If something doesnt work with this setup, just ask, most probably i've already run through that problem

            Regards,
            Indy

            Comment


              #7
              Nice tutorial The OnFindOnlineGamesComplete delegate doesn't always trigger in a timely fashion, because the internal Steam code (in the SteamSDK) doesn't always trigger that event properly).

              I'd advise adding a SetTimer for timing out the search results after a period, and then cancelling any active queries; cancelling should then trigger OnFindOnlineGamesComplete.

              Comment


                #8
                Hi!

                First of all thanks for this tutorial. I managed to run my server and query it. (Server and Client running on the same machine) I also managed to add the server to my serverbrowser. The only thing that I am not getting to run is joining my server. The client tries to connect and throws me back to the main menu. In the log are various messages. I also noticed that the SteamServerId is empty. The server´s log looks like this:

                Log: LoadMap: bl-khorartifact?Name=Mercenary?Team=255?game=Cruzade. CRZBloodLust?SteamServerId=?NumPublicConnections=1 2?NumPrivateConnections=12?NumOpenPublicConnection s=12?NumOpenPrivateConnections=12?bShouldAdvertise =True?bIsLanMatch=False?bUsesStats=True?bAllowJoin InProgress=True?bAllowInvites=True?bUsesPresence=T rue?bAllowJoinViaPresence=True?bAllowJoinViaPresen ceFriendsOnly=False?bUsesArbitration=False?bAntiCh eatProtected=False?bIsDedicated=False?PingInMs=0?M atchQuality=0.000000?GameState=OGS_Pending?ServerN ame=Testchamber?listen?steamsockets

                ScriptWarning: Invalid user index (255) specified for ClearReadProfileSettingsCompleteDelegate() OnlineSubsystemSteamworks Transient.OnlineSubsystemSteamworks_0
                Function OnlineSubsystemSteamworks.OnlineSubsystemSteamwork s:ClearReadProfileSettingsCompleteDelegate:00FE

                Init: WinSock: Socket queue 131072 / 131072
                Log: Initialized RedirectNetDriver, for redirecting IP connections to Steam sockets
                Log: NetMode is now 2
                Log: Steam game server UID: 90085035492219908
                The clients log shows

                Init: WinSock: Socekt queue 32768 / 32768
                NetComeGo: Close TcpNetDriver_0 TcpipConnection_0 <MyGlobalIPAdress>:7777 07/31/12 10:08:56
                ScriptLog: LocalPlayer::StaticOnServerConnectionClose: Coulnd´t find local client auth session
                LocalPlayer::StaticOnServerConnectionClose: Coulnd´t find local server auth session
                Log: Pending connect to <MyGlobalIPAdress>/CRZMainMenu?Name=Mercenary?Team=255 failed; Duplicate UID
                ScriptLog: <CRZGameCiewportClient_0> UTGameViewportClient::None:NotifyConnectionError Message: Duplicate UID Title: Connection Lost
                Log: Failed; returning to Entry
                I hope someone can help me figuring out why my connections fail.

                Edit: I already opened all ports needed for steam and the UDK.

                Comment


                  #9
                  Hi,
                  i guess your problem is that your are trying to host the game with a listen server and join the game both with the same Steam-Account; That will not work, as Steam doesnt allow duplicate logins.
                  You will either have to start a dedicated server, then you will be able to login with a client, or use a second steam id (on another pc). But note that you will have to have the same binaries as the server or else the connection will be refused here too.
                  And yes, i know, its a pain in the *** to debug that way....

                  Comment


                    #10
                    Thank you very much indygoof!

                    Yeah, its really a pity. Do you know what the variable "bFilterEngineBuild" in the DefaultEngine.ini at the [OnlineSubsystemSteamworks.OnlineSubsystemSteamwork s] does? I assumed that, if set to false, will not check if the binarys are exactly the same.
                    Its really unconvenient to test internet multiplayer when you need to build an installer each time to assure that everyone on the team has exactly the same compiled files.

                    Comment


                      #11
                      I didnt see that variable before....but the final decision if you are allowed to join is done on the server and has nothing to do with the type of the onlinesubsystem.
                      I think that variable is there so you only see the servers running with the same build-version as your client (which is good since you wont be able to connect anyways).

                      Maybe you will feel better knowing that i also had really much "fun" with debugging issues with my steam-implementation

                      Comment


                        #12
                        I see. That makes sense. A last question. Do you also know the exact difference between ODAT_OnlineService and ODAT_QoS? I cant find any information about that.

                        Comment


                          #13
                          There shouldn't be a difference between any ODAT values as the Steam subsystem does not use them. The "duplicate uid" error you get when trying to connect, happens when multiple clients with the same UID try to connect to a server; not sure how you're triggering that, unless you are trying to run two UDK clients from the same Steam instance.

                          Comment


                            #14
                            Followed the OP and managed to get a listen server advertising and could see it in search results.

                            However I had to open up port 27015 to be able to see it in a search and once I had that done doing a search on a different PC on the same network with a different steam account couldn't see the server so I still couldn't connect to it.

                            One thing I've noticed is that if I host a listen server this way the join/invite functionality in Steam doesn't light up for friends.

                            EDIT: Now tried it on two machines other than my Dev machine and the only time I can ever see any servers at all is on the Dev machine. I've tried 2 different steam accounts and made sure all the ports were pointing to the right machine and it sees absolutely nothing according to the DevOnline logs. In comparison on my Dev machine it tends to see my server and a couple of other random UDK servers.

                            Comment


                              #15
                              Well I am experiencing almost the same. Did you use the same compiled scripts on both machines? Dont know how to archive it. I tried to send my compiled scripts to my friend and he tried to find my game but it did not work. I only managed to find my server when hosting a dedicated server via the modified UDKFrontend.

                              Is it possible to tell the Engine to ignore different scripts in dev mode? (Debug compile)

                              Comment

                              Working...
                              X