UDN
Search public documentation:

KismetOnlineSubsystem
日本語訳
中国翻译
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 Home > Unreal Development Kit Gems > Kismet Online Subsystem

Kismet Online Subsystem


Last tested against UDK Jan, 2012
PC compatible
iOS compatible

Overview


Kismet is visual scripting system designed to allow you to build game play logic using nodes. It's a very useful tool all the way from the prototype phase to the production phase. Because some of the game play logic exists within Kismet, it is only natural to also want to be able to access Game Center or Steam Works through Kismet as well. This development kit gems adds new Kismet nodes which allows you to interface with Game Center or Steam Works.

ALERT! Note: While the Unrealscript provided defines the same Kismet nodes for both Game Center and Steam Works, only one can be used at a time. This is because the preprocessor will transform the Unrealscript to a specific version. This was required as Game Center and Steam Works function differently from each other. Thus if you are making a multi platform game, you may need to create two different versions of the Kismet. The best way to do this is to have a streaming level which contains the Game Center Kismet nodes and another one which contains the Steam Kismet nodes.

A word about Unrealscript preprocessing


The preprocessor in Unrealscript works very similar to how the preprocessor works in other languages such as C++. A preprocessor is a very simple script which is able to define constants and include/exclude portions of Unrealscript depending on if and else statements.

In this snippet of code, there are two sections of code which the preprocessor will include depending on the constants defined. If USE_STEAMWORKS is defined, then everything inside the if statement is included for compilation. Otherwise if USE_STEAMWORKS is not defined, then everything inside the else statement is included for compilation. Due to the difference between the way Game Center and Steam Works work, it was required to have different pathways for code to run.

SeqAct_UnlockAchievement.uc
/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  local OnlineSubsystem OnlineSubsystem;

  // Connect to the online subsystem and link up the achievement delegates
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.PlayerInterface != None)
  {
// ==========
// SteamWorks
// ==========
`if(`isdefined(USE_STEAMWORKS))
    // Begin unlocking the achievement
    BeginUnlockAchievement();

// ==========
// GameCenter
// ==========
`else
    // Game Center requires you to read the achievements list first, and then unlock the achievement.
    // Assign the read achievements delegate
    OnlineSubsystem.PlayerInterface.AddReadAchievementsCompleteDelegate(ProcessingPlayers[0].LocalUserNum, InternalOnReadAchievementsComplete);

    // Read all achievements 
    OnlineSubsystem.PlayerInterface.ReadAchievements(ProcessingPlayers[0].LocalUserNum);
`endif
  }
}

USE_STEAMWORKS and USE_GAMECENTER are defined in a file called Globals.uci. Comment out the one which you don't want to use. When you modify Globals.uci, the Unrealscript Compiler won't automatically pick this up. Thus you'll need force a recompilation of Unrealscripts when you change it.

A word about Garbage Collection


A lot of the tasks that the Online Subsystem performs are done asynchronously. Thus, a lot of tasks use delegates which are called when they've finished. The Kismet nodes themselves bind to the delegates, which if not cleaned up properly cause the level to not be garbage collected and removed from memory. An interaction called GameKismetSceneClient is used to detect when a game session has ended at which time it asks all of the Kismet nodes to clean themselves up.

GameKismetSceneClient.uc
/**
 * A GameKismetSceneClient is an interaction which watches when a game session ends. This is required to clean up all of the Kismet nodes that may have
 * bindings to the OnlineSubsystems
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class GameKismetSceneClient extends Interaction;

/**
 * Called when the current game session has ended. Cleans up all OnlineSubsystemBase Kismet nodes
 */
function NotifyGameSessionEnded()
{
  local Sequence GameSequence;
  local array<SequenceObject> SequenceObjects;
  local SeqAct_OnlineSubsystemBase SeqAct_OnlineSubsystemBase;
  local int i;
  local WorldInfo WorldInfo;

  // Get the world info
  WorldInfo = class'WorldInfo'.static.GetWorldInfo();
  if (WorldInfo == None)
  {
    return;
  }
   
  // Clean up all of the online subsystem base Kismet nodes
  GameSequence = WorldInfo.GetGameSequence();
  if (GameSequence != None)
  {
    GameSequence.FindSeqObjectsByClass(class'SeqAct_OnlineSubsystemBase', true, SequenceObjects);
    if (SequenceObjects.Length > 0)
    {
      for (i = 0; i < SequenceObjects.Length; ++i)
      {
        SeqAct_OnlineSubsystemBase = SeqAct_OnlineSubsystemBase(SequenceObjects[i]);
        if (SeqAct_OnlineSubsystemBase != None)
        {
          SeqAct_OnlineSubsystemBase.CleanUp(true);
        }
      }
    }
  }
}

defaultproperties
{
}

Steam Works and Game Center Kismet Nodes


SeqAct_OnlineSubsystemBase

SeqAct_OnlineSubsystemBase is an abstract class where all of the other Kismet Sequence Actions are based of. This base class handles queuing online subsystem instructions, organizing player information and clean up. It also handles most of the output link calls. If you need to create a new Kismet Sequence Action which utilizes any of the online subsystem then it is best to subclass this class. When this Kismet Sequence Action is activated, it will compile an array of players that it needs to work on based on the attached Kismet Variable Players node attached. It then processes the players array sequentially. This Kismet Sequence Action will output to Out immediately. If the Kismet Sequence Action is busy processing players, it will output to Busy immediately. Depending on the results of the subclassed Kismet Sequence Action it may later output to Succeeded or Failed.

Functions

  • Activated - This event is called when the Kismet Sequence Action is activated by another Kismet Sequence node.
  • FinishedProcessPlayerIndex - This function is call from subclasses when it has finished processing what it needs to do for a player. If bWasSuccessful is true, then the Succeeded output is activated, otherwise the Failed output is activated. Set bProcessedAllPlayers if the subclassed Kismet node has processed all players at once.
  • InternalOnActivated - This function is a stub function which is called within Activated. This is for subclasses to extend, as subclasses should not extend the Activated event.
  • CleanUp - This function is called from GameKismetSceneClient and from within FinishedProcessPlayerIndex. This function should be extended by subclasses that bind to delegates or require some other kind of clean up. If IsExiting is true, then you should clean everything up.
  • NotifyGameKismetSceneClientCreated - This function is called by another SeqAct_OnlineSubsystemBase to indicate that the GameKismetSceneClient has already been created. When a SeqAct_OnlineSubsystemBase is first activated, it will first attempt to create a GameKismetSceneClient to insert into the local player's viewport. When that succeeds, it tells all other SeqAct_OnlineSubsystemBase's to not attempt to create it again.

Definitions

  • SProcessingPlayer - This struct defines the player that is being processed or pending processing.
  • IsProcessing - When this is true, then any more requests to this Kismet node are immediately rejected as this Kismet node is busy doing a task.
  • ProcessingPlayers - The array of players currently being processed. Players are processing sequentially. After each player is processed, it then removes the first entry from the array and then deals with the next entry until this array is empty.
  • HasCreatedSceneClient - When this is false, when this Kismet Sequence Action is activated it will attempt to create a GameKismetSceneClient and insert it into the local player's viewport. This is to catch when the game session has ended so that clean up can occur. If this is true, then this Kismet Sequence Action does not attempt to do this.

Execution flow

When this Kismet Sequence Action is activated by another Kismet Sequence Node, Activated() is first called. Activated() checks to see if it needs to create the GameKismetSceneClient, by checking the value of HasCreatedSceneClient. If it does, then it grabs the local player controller and inserts a new GameKismetSceneClient into the viewport. If that succeeds, it then informs all other SeqAct_OnlineSubsystemBase that this has been done and they don't need to do it. If IsProcessing is true, then the "Busy" output is activated and the function stops. Otherwise, it compiles the list of players based on the attached Kismet Player Variable. For each player listed in the Kismet Player Variable, it fills the ProcessingPlayers array with the player index and unique player id. If the ProcessingPlayers array has a size, IsProcessing is flagged true and InternalOnActivated() is called. The "Out" output is then activated.

When the subclass has finished processing a player, FinishedProcessPlayerIndex() is called. If bProcessedAllPlayers is true, then the ProcessingPlayers array is emptied. Otherwise the first entry in ProcessingPlayers is removed only. If the ProcessingPlayers is not empty, then the first entry is then processed by calling InternalOnActivated() again. Else if bWasSuccessful is true, then the "Succeeded" output is activated; otherwise the "Failed" output is activated.

When CleanUp() is called and IsExiting is true, then ProcessingPlayers array is emptied.

When NotifyGameKismetSceneClientCreated() is called, HasCreatedSceneClient is set true.

Unrealscript

SeqAct_OnlineSubsystemBase.uc
/**
 * An OnlineSubsystemBase action is an abstract Action which is used to call other online subsystem functions which require local user nums
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_OnlineSubsystemBase extends SequenceAction
  abstract
  HideDropDown;

struct SProcessingPlayer
{
  var byte LocalUserNum;
  var UniqueNetId LocalNetId;
};

var private bool IsProcessing;
var protected array<SProcessingPlayer> ProcessingPlayers;
var protected bool HasCreatedSceneClient;

/**
 * Called when this event is activated.
 */
event Activated()
{
  local SeqVar_Player SeqVar_Player;
  local bool AllPlayers;
  local array<int> PlayerIndices, ExistingPlayerIndices;
  local WorldInfo WorldInfo;
  local PlayerController PlayerController;
  local LocalPlayer LocalPlayer;
  local int i;
  local SProcessingPlayer ProcessingPlayer;
  local OnlineSubSystem OnlineSubSystem;
  local Sequence GameSequence;
  local array<SequenceObject> SequenceObjects;
  local SeqAct_OnlineSubsystemBase SeqAct_OnlineSubsystemBase;
  
  // Check if we need to insert the GameKismetSceneClient to watch for garbage collection events
  if (!HasCreatedSceneClient)
  {
    // Insert the game kismet scene client
    WorldInfo = GetWorldInfo();
    if (WorldInfo != None)
    {
      // Get the local player controller
      PlayerController = WorldInfo.GetALocalPlayerController();
      if (PlayerController != None)
      {
        LocalPlayer = LocalPlayer(PlayerController.Player);
        if (LocalPlayer != None && LocalPlayer.ViewportClient != None)
        {
          LocalPlayer.ViewportClient.InsertInteraction(new (LocalPlayer.ViewportClient) class'GameKismetSceneClient');
        }
      }
    }

    // Flag all SeqAct_OnlineSubsystemBase that the game kismet scene client has been created
    GameSequence = WorldInfo.GetGameSequence();
    if (GameSequence != None)
    {
      GameSequence.FindSeqObjectsByClass(class'SeqAct_OnlineSubsystemBase', true, SequenceObjects);
      if (SequenceObjects.Length > 0)
      {
        for (i = 0; i < SequenceObjects.Length; ++i)
        {
          SeqAct_OnlineSubsystemBase = SeqAct_OnlineSubsystemBase(SequenceObjects[i]);
          if (SeqAct_OnlineSubsystemBase != None)
          {
            SeqAct_OnlineSubsystemBase.NotifyGameKismetSceneClientCreated();
          }
        }
      }
    }
  }

  if (IsProcessing)
  {
    // Activate the Busy output
    ActivateOutputLink(1);
    return;
  }

  // Get the online subsystem
  OnlineSubSystem = class'GameEngine'.static.GetOnlineSubSystem();
  if (OnlineSubSystem == None || OnlineSubSystem.PlayerInterface == None)
  {
    return;
  }

  // Get the player indices
  ForEach LinkedVariables(class'SeqVar_Player', SeqVar_Player)
  {
    if (SeqVar_Player.bAllPlayers)
    {
      AllPlayers = true;
      break;
    }
    else
    {
      PlayerIndices.AddItem(SeqVar_Player.PlayerIdx);
    }
  }

  // Find the players that need to be processed
  WorldInfo = GetWorldInfo();
  if (WorldInfo != None)
  {
    // If all players is true, then iterate for each player
    if (AllPlayers)
    {
      ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController)
      {
        LocalPlayer = LocalPlayer(PlayerController.Player);
        if (LocalPlayer != None)
        {
          // Create the processing player struct
          ProcessingPlayer.LocalUserNum = class'UIInteraction'.static.GetPlayerIndex(LocalPlayer.ControllerId);
          OnlineSubSystem.PlayerInterface.GetUniquePlayerId(ProcessingPlayer.LocalUserNum, ProcessingPlayer.LocalNetId);

          // Append the processing player struct to the processing players array
          ProcessingPlayers.AddItem(ProcessingPlayer);
        }
      }
    }
    // Otherwise iterate for each player index
    else if (PlayerIndices.Length > 0)
    {
      // Get all of the existing player indices first
      ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController)
      {
        LocalPlayer = LocalPlayer(PlayerController.Player);
        if (LocalPlayer != None)
        {
          ExistingPlayerIndices.AddItem(class'UIInteraction'.static.GetPlayerIndex(LocalPlayer.ControllerId));
        }
      }

      for (i = 0; i < PlayerIndices.Length; ++i)
      {
        if (ExistingPlayerIndices.Find(PlayerIndices[i]) != INDEX_NONE)
        {
          // Create the processing player struct
          ProcessingPlayer.LocalUserNum = PlayerIndices[i];
          OnlineSubSystem.PlayerInterface.GetUniquePlayerId(ProcessingPlayer.LocalUserNum, ProcessingPlayer.LocalNetId);

          // Append the processing player struct to the processing players array
          ProcessingPlayers.AddItem(ProcessingPlayer);
        }
      }
    }

    // Process the first one
    if (ProcessingPlayers.Length > 0)
    {
      // Set processing to true
      IsProcessing = true;

      // Activate
      InternalOnActivated();
    }
  }

  // Activate the Out output
  ActivateOutputLink(0);
}

/**
 * Called when the Kismet node has finished processing for this player
 *
 * @param      bWasSuccessful        True if the Kismet node was successful in what it was doing
 * @param      bProcessedAllPlayers    True if this Kismet node processed all players in one go, rather then doing one player at a time
 */
protected function FinishedProcessPlayerIndex(bool bWasSuccessful, optional bool bProcessedAllPlayers)
{
  // Perform clean up of this Kismet node
  CleanUp();

  // Pop the first processing player index
  if (ProcessingPlayers.Length > 0)
  {
    // If processed all players, then remove all of them now
    if (bProcessedAllPlayers)
    {
      ProcessingPlayers.Remove(0, ProcessingPlayers.Length);
    }
    // Otherwise we've only processed one, so pop it off the top
    else
    {
      ProcessingPlayers.Remove(0, 1);
    }
  }

  // If there is still more player indices to process, process the next one
  if (ProcessingPlayers.Length > 0)
  {
    InternalOnActivated();
  }
  // Otherwise, this Kismet node has finished processing and should activate one of the outputs
  else
  {
    IsProcessing = false;
    ForceActivateOutput((bWasSuccessful) ? 2 : 3);
  }  
}

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated();

/**
 * Called when this Kismet node should clean up all variable or delegate references so that garbage collection can occur
 *
 * @param    IsExiting      If exiting, then clean up everything
 */
function CleanUp(optional bool IsExiting)
{
  // Clear processing players array
  if (IsExiting)
  {
    ProcessingPlayers.Remove(0, ProcessingPlayers.Length);
  }
}

/**
 * Called when a Kismet node has created the GameKismetSceneClient which is used to catch when the game session has finished and when game clean up should occur
 */
function NotifyGameKismetSceneClientCreated()
{
  HasCreatedSceneClient = true;
}

defaultproperties
{
`if(`isdefined(USE_STEAMWORKS))
  ObjCategory="Steamworks"
`elseif(`isdefined(USE_GAMECENTER))
  ObjCategory="Game Center"
`endif
  ObjColor=(R=0,G=63,B=127,A=255)

  bAutoActivateOutputLinks=false

  VariableLinks.Empty
  VariableLinks(0)=(ExpectedType=class'SeqVar_Player',LinkDesc="Player")

  OutputLinks(1)=(LinkDesc="Busy")
  OutputLinks(2)=(LinkDesc="Succeeded")
  OutputLinks(3)=(LinkDesc="Failed")
}


SeqAct_UnlockAchievement

This Kismet Sequence Action unlocks an achievement based on the Id attached or defined in the properties panel.

KismetOnlineSubsystem_00.png

KismetOnlineSubsystem_07.jpg

Functions

  • InternalOnActivated - This function is called by the parent class, SeqAct_OnlineSubsystemBase. Game Center first requires you to read the achievements before unlocking any thus OnlineSubsystem.PlayerInterface.ReadAchievements() is called. Steamworks just called BeginUnlockAchievement().
  • InternalOnReadAchievementsComplete - This is called from OnlineSubsystem.PlayerInterface via a delegate. When this is called, it checks if the achievement has been achieved online. If it has been and AlwaysUnlockAchievement is true, then InternalOnUnlockAchievementComplete is called. Otherwise BeginUnlockAchievement() is called.
  • BeginUnlockAchievement - This sends a call to OnlineSubsystem.PlayerInterface.UnlockAchievement().
  • InternalOnUnlockAchievementComplete - This is called from OnlineSubsystem.PlayerInterface via a delegate. When this is called, it calls FinishedProcessPlayerIndex().
  • CleanUp - This handles cleaning up all of the bound delegates.

Definitions

  • AchievementId - The Id of the achievement you wish to unlock. This can also be defined by attaching a Kismet Int Variable to the variable links.
  • AlwaysUnlockAchievement - If true, then the Kismet Sequence Action will still output "Succeeded" even if the achievement has already been unlocked. Game Center only
  • DownloadedAchievements - An array of the achievements downloaded from an online source. Game Center only

Game Center Execution flow

When InternalOnActivated() is called, a delegate is added to OnlineSubsystem.PlayerInterface which is called when OnlineSubsystem.PlayerInterface.ReadAchievements has completed.

When the online subsystem has finished reading the achievements online, it then calls InternalOnReadAchievementsComplete(). InternalOnReadAchievementsComplete() checks to see if the achievement has already been unlocked or not. If it has not been unlocked, then BeginUnlockAchievement() is called. Otherwise if AlwaysUnlockAchievement is true, then InternalOnUnlockAchievementComplete() is called. BeginUnlockAchievement() binds to another online subsystem delegate which is called when unlocking the achievement finishes. OnlineSubsystem.PlayerInterface.UnlockAchievement() is then called.

When the achievement has finished unlocking, InternalOnUnlockAchievementComplete() is then called which calls FinishedProcessPlayerIndex().

When CleanUp is called, all of the bound delegates are removed.

Steamworks Execution flow

When InternalOnActivated() is called BeginUnlockAchievement() is called. BeginUnlockAchievement() binds to another online subsystem delegate which is called when unlocking the achievement finishes. OnlineSubsystem.PlayerInterface.UnlockAchievement() is then called.

When the achievement has finished unlocking, InternalOnUnlockAchievementComplete() is then called which calls FinishedProcessPlayerIndex().

When CleanUp is called, all of the bound delegates are removed.

Unrealscript

SeqAct_UnlockAchievement.uc
/**
 * An UnlockAchievement Action is used to unlock an achievement.
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_UnlockAchievement extends SeqAct_OnlineSubsystemBase;

// Achievement Id that you want to unlock
var() int AchievementId;

// ==========
// GameCenter
// ==========
`if(`isdefined(USE_GAMECENTER))
// If true, then achivements that have already been unlocked, will still output the success link
var() bool AlwaysUnlockAchievement;
// Array of all downloads achievements
var protected array<AchievementDetails> DownloadedAchievements;
`endif

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  local OnlineSubsystem OnlineSubsystem;

  // Connect to the online subsystem and link up the achievement delegates
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.PlayerInterface != None)
  {
// ==========
// SteamWorks
// ==========
`if(`isdefined(USE_STEAMWORKS))
    // Begin unlocking the achievement
    BeginUnlockAchievement();

// ==========
// GameCenter
// ==========
`else
    // Game Center requires you to read the achievements list first, and then unlock the achievement.
    // Assign the read achievements delegate
    OnlineSubsystem.PlayerInterface.AddReadAchievementsCompleteDelegate(ProcessingPlayers[0].LocalUserNum, InternalOnReadAchievementsComplete);

    // Read all achievements 
    OnlineSubsystem.PlayerInterface.ReadAchievements(ProcessingPlayers[0].LocalUserNum);
`endif
  }
}

// ==========
// GameCenter
// ==========
`if(`isdefined(USE_GAMECENTER))
/**
 * Called when the async achievements read has completed.
 *
 * @param    TitleId      The title id that the read was for (0 means current title)
 */
protected function InternalOnReadAchievementsComplete(int TitleId)
{
  local OnlineSubsystem OnlineSubsystem;
  local int AchievementIndex;

  // Ensure we have an online subsystem, and an associated player interface
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem == None || OnlineSubsystem.PlayerInterface == None)
  {
    return;
  }

  // Read the achievements into the downloaded achievements array
  OnlineSubsystem.PlayerInterface.GetAchievements(ProcessingPlayers[0].LocalUserNum, DownloadedAchievements, TitleId);

  // Grab the achievement index
  AchievementIndex = DownloadedAchievements.Find('Id', AchievementId);

  // Unlock the achievement    
  if (AchievementIndex != INDEX_NONE)
  {
    // We haven't unlocked it yet, so start the unlock process
    if (!DownloadedAchievements[AchievementIndex].bWasAchievedOnline)
    {
      BeginUnlockAchievement();
    }
    // We've already unlocked it, so finish 
    else if (AlwaysUnlockAchievement)
    {
      InternalOnUnlockAchievementComplete(true);
    }
  }
}
`endif

/**
 * Called when unlocking the achievement should begin
 *
 * @param    AchievementId      Which achievement to unlock
 * @param    LocalUserNum      Local user index
 */
protected function BeginUnlockAchievement()
{
  local OnlineSubsystem OnlineSubsystem;

  // Grab the online subsystem
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem == None || OnlineSubsystem.PlayerInterface == None)
  {
    return;
  }

  // Assign the unlock achievement complete delegate
  OnlineSubsystem.PlayerInterface.AddUnlockAchievementCompleteDelegate(ProcessingPlayers[0].LocalUserNum, InternalOnUnlockAchievementComplete);
      
  // Start the unlocking process
  OnlineSubsystem.PlayerInterface.UnlockAchievement(ProcessingPlayers[0].LocalUserNum, AchievementId);
}

/**
 * Called when the achievement unlocking has completed
 *
 * @param     bWasSuccessful      True if the async action completed without error, false if there was an error
 */
protected function InternalOnUnlockAchievementComplete(bool bWasSuccessful)
{
  // Finished unlocking this achievement
  FinishedProcessPlayerIndex(bWasSuccessful);
}

/**
 * Called when this Kismet node should clean up all variable or delegate references so that garbage collection can occur
 *
 * @param    IsExiting      If exiting, then clean up everything
 */
function CleanUp(optional bool IsExiting)
{
  local OnlineSubsystem OnlineSubsystem;
  local int i, InPlayerIndex;

  // Grab the online subsystem and remove all delegate binds
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.PlayerInterface != None)
  {
    // There can only be 4 maximum split screen local players
    for (i = 0; i < 4; ++i)
    {
      // Get the player index
      InPlayerIndex = class'UIInteraction'.static.GetPlayerIndex(i);
      if (InPlayerIndex >= 0)
      {
      // Clear the delegates
// ==========
// GameCenter
// ==========
`if(`isdefined(USE_GAMECENTER))
        OnlineSubsystem.PlayerInterface.ClearReadAchievementsCompleteDelegate(InPlayerIndex, InternalOnReadAchievementsComplete);
`endif
        OnlineSubsystem.PlayerInterface.ClearUnlockAchievementCompleteDelegate(InPlayerIndex, InternalOnUnlockAchievementComplete);
      }
    }
  }

  // Call the super just in case we are exiting
  Super.CleanUp(IsExiting);
}

defaultproperties
{
// ==========
// GameCenter
// ==========
`if(`isdefined(USE_GAMECENTER))
  AlwaysUnlockAchievement=true
`endif
  ObjName="Unlock Achievement"
  VariableLinks(1)=(ExpectedType=class'SeqVar_Int',LinkDesc="Achievement Id",PropertyName=AchievementId)
}

Example Kismet

KismetOnlineSubsystem_10.png


SeqAct_ModifyOnlineStat

Modify Online Stat is a Kismet Sequence Action which allows you to modify a stat online used by the online subsystem. For Game Center, this system allows you to upload new stats to use on leaderboards. For Steamworks this system allows you to add, subtract or set stats to use for achievements.

KismetOnlineSubsystem_01.png

KismetOnlineSubsystem_08.jpg

Functions

  • InternalOnActivated - Called from the parent class, SeqAct_OnlineSubsystemBase. On Game Center this will always just call SetStatValue(). On Steamworks, depending on that value of ModifyMethod, this may call either ReadOnlineStats() (if you wanted to add or subtract for existing stats) or SetStatValue().
  • ReadOnlineStats - This is only called on the Steamworks version when you want to add or subtract from an existing stat. This reads the online stat, and then calls InternalOnReadOnlineStatsComplete() when it is finished.
  • InternalOnReadOnlineStatsComplete - After the stats have been read, the stat is either added to or subtracted from and then the result is sent back to Steamworks.
  • SetStatValue - This sets the stats and sends it back to either Game Center or Steamworks.
  • InternalOnFlushOnlineStatsComplete - This is only called on the Steamworks version when the stats have been written to the online database.
  • CleanUp - This is called when the Kismet node should clean itself up.

Definitions

  • SStat - Base struct definition. StatId refers the the stat id on Game Center or Steamworks. LinkedVariableName allows you to link this stat id to an attached Kismet Variable.
  • SIntStat - Extension of SStat. Defines a value to add, subtract or set the stat.
  • SFloatStat - Extension of SStat. Defines a value to add, subtract or set the stat.
  • EModifyMethod - This sets how you want to modify the online stat.
  • OnlineStatsWriteClass - This defines which OnlineStatsWrite to instance when writing to the online subsystem.
  • OnlineStatsReadClass - This defines which OnlineStatsRead to instance when writing to the online subsystem.
  • IntStats - Array of stat definitions to upload to the online subsystem.
  • FloatStats - Array of stat definitions to upload to the online subsystem.
  • ModifyMethod - Defines which method to modify the online stat. Only set is available on Game Center, Steamworks is able to add, subtract or set.
  • SessionName - Defines the session name, this is defaulted to "Game".

Game Center Execution flow

On GameCenter, when InternalOnActivated() is called, SetStatValue() is called immediately. SetStatValue() instances an OnlineStatsWrite using the class defined in OnlineStatsWriteClass. It then iterates over IntStats and FloatStats and writes the values of those stats to the instanced OnlineStatsWrite class. It then writes these stats online and calls FinishedProcessPlayerIndex().

When CleanUp() is called, all delegates bound are freed. Any object references are also cleared.

Steamworks Execution flow

On Steamworks, when InternalOnActivated() is called depending on the value of ModifyMethod two functions can be called. If ModifyMethod is MM_Add or MM_Subtract then ReadOnlineStats() is called, this is because the online stats value needs to be read so that it can be modified either by adding to it or subtracting from it. Otherwise SetStatValue() is called.

When ReadOnlineStats() is called, a delegate is bound and an instance of OnlineStatsRead is created. OnlineSubsystem.StatsInterface.ReadOnlineStats() is then called. When reading the online stats is finished, InternalOnReadOnlineStatsComplete is then called. InternalOnReadOnlineStatsComplete then interates over the IntStats and FloatStats arrays and modifies the stats values. The modified stats are then written to an OnlineStatsWrite instance and are then sent to Steamworks by calling OnlineSubsystem.StatsInterface.WriteOnlineStats(). If OnlineSubsystem.StatsInterface.WriteOnlineStats() passed, then a delegate is bound and OnlineSubsystem.StatsInterface.FlushOnlineStats() is called. When the online stats has been flushed, InternalOnFlushOnlineStatsComplete() is called which then calls FinishedProcessPlayerIndex().

When SetStatValue() is called, it interates over the IntStats and FloatStats arrays and sets the stats values. The modified stats are then written to an OnlineStatsWrite instance and are then sent to Steamworks by calling OnlineSubsystem.StatsInterface.WriteOnlineStats(). If OnlineSubsystem.StatsInterface.WriteOnlineStats() passed, then a delegate is bound and OnlineSubsystem.StatsInterface.FlushOnlineStats() is called. When the online stats has been flushed, InternalOnFlushOnlineStatsComplete() is called which then calls FinishedProcessPlayerIndex().

When CleanUp() is called, all delegates bound are freed. Any object references are also cleared.

Unrealscript

SeqAct_ModifyOnlineStat.uc
/**
 * An Modify Online Stat Action is used to modify and upload stats.
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_ModifyOnlineStat extends SeqAct_OnlineSubsystemBase;

// Stat base struct
struct SStat
{
  var() Name LinkedVariableName;
  var() int StatId;
};

// Integer based stat
struct SIntStat extends SStat
{
  var() int Value;
};

// Float based stat
struct SFloatStat extends SStat
{
  var() float Value;
};

// What method does the user want to modify the stat
enum EModifyMethod
{
  MM_Set<DisplayName=Set>,
// ==========
// Steamworks
// ==========
`if(`isdefined(USE_STEAMWORKS))
  MM_Add<DisplayName=Add>,
  MM_Subtract<DisplayName=Subtract>,
`endif
};

// Online stats write class associated with the stats
var() class<OnlineStatsWrite> OnlineStatsWriteClass;
// ==========
// Steamworks
// ==========
`if(`isdefined(USE_STEAMWORKS))
// Online stats read class associated with the stats
var() class<OnlineStatsRead> OnlineStatsReadClass;
`endif
// Array of integer based stats
var() array<SIntStat> IntStats;
// Array of float based stats
var() array<SFloatStat> FloatStats;
// Method to modify the stat
var() EModifyMethod ModifyMethod;
// Session name
var() Name SessionName;

// Online stats write object instance
var protected OnlineStatsWrite OnlineStatsWrite;
// Online stats read object instance
var protected OnlineStatsRead OnlineStatsRead;

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  // Abort if the online stats write class is none
  // Abort if there are no column ids
  if (OnlineStatsWriteClass == None || IntStats.Length <= 0)
  {
    return;
  }
  
// ==========
// Steamworks
// ==========
`if(`isdefined(USE_STEAMWORKS))
  // User wants to modify the stats using add or subtract
  if (ModifyMethod == MM_Add || ModifyMethod == MM_Subtract)
  {
    ReadOnlineStats();
  }
  // User wants to modify the stats by setting the value
  else
  {
`endif
    SetStatValue();
// ==========
// Steamworks
// ==========
`if(`isdefined(USE_STEAMWORKS))
  }
`endif
}

// ==========
// Steamworks
// ==========
`if(`isdefined(USE_STEAMWORKS))
/**
 * Called when the user wants to modify the stats using increment or decrement
 */
protected function ReadOnlineStats()
{
  local OnlineSubsystem OnlineSubsystem;
  local array<UniqueNetId> PlayerIds;

  // Attempt to read the online stats
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.StatsInterface != None)
  {
    OnlineSubsystem.StatsInterface.AddReadOnlineStatsCompleteDelegate(InternalOnReadOnlineStatsComplete);

    PlayerIds.AddItem(ProcessingPlayers[0].LocalNetId);
    OnlineStatsRead = new () OnlineStatsReadClass;
    if (OnlineStatsRead != None)
    {
      OnlineSubsystem.StatsInterface.ReadOnlineStats(PlayerIds, OnlineStatsRead);
    }
  }
}

/**
 * Called when reading the online stats has finished
 *
 * @param    bWasSuccessful      True if reading the online stats succeeded
 */
function InternalOnReadOnlineStatsComplete(bool bWasSuccessful)
{
  local OnlineSubsystem OnlineSubsystem;
  local int i, ReadIntStatValue;
  local float ReadFloatStatValue;
  local SeqVar_Int SeqVar_Int;
  local SeqVar_Float SeqVar_Float;

  // If reading the stats is successful
  if (bWasSuccessful && OnlineStatsRead != None)
  {
    // Write stats
    OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
    if (OnlineSubsystem != None && OnlineSubsystem.StatsInterface != None)
    {    
      // Instance the stats write object
      OnlineStatsWrite = new () OnlineStatsWriteClass;
      if (OnlineStatsWrite != None)
      {
        // Modify the int stats write values
        for (i = 0; i < IntStats.Length; ++i)
        {
          OnlineStatsRead.GetIntStatValueForPlayer(ProcessingPlayers[0].LocalNetId, IntStats[i].StatId, ReadIntStatValue);

          // If this state has a variable name, then perform a look up
          if (IntStats[i].LinkedVariableName != '' && IntStats[i].LinkedVariableName != 'None')
          {
            ForEach LinkedVariables(class'SeqVar_Int', SeqVar_Int)
            {
              if (SeqVar_Int.VarName == IntStats[i].LinkedVariableName)
              {
                switch (ModifyMethod)
                {              
                case MM_Add:
                  OnlineStatsWrite.SetIntStat(IntStats[i].StatId, ReadIntStatValue + SeqVar_Int.IntValue);
                  break;

                case MM_Subtract:
                  OnlineStatsWrite.SetIntStat(IntStats[i].StatId, ReadIntStatValue - SeqVar_Int.IntValue);
                  break;

                default:
                  break;
                }
              }
            }
          }
          // Otherwise use the value defined in the struct
          else
          {
            switch (ModifyMethod)
            {              
            case MM_Add:
              OnlineStatsWrite.SetIntStat(IntStats[i].StatId, ReadIntStatValue + IntStats[i].Value);
              break;

            case MM_Subtract:
              OnlineStatsWrite.SetIntStat(IntStats[i].StatId, ReadIntStatValue - IntStats[i].Value);
              break;

            default:
              break;
            }
          }
        }

        // Modify the float stats write values
        for (i = 0; i < FloatStats.Length; ++i)
        {
          OnlineStatsRead.GetFloatStatValueForPlayer(ProcessingPlayers[0].LocalNetId, FloatStats[i].StatId, ReadFloatStatValue);

          // If this state has a variable name, then perform a look up
          if (FloatStats[i].LinkedVariableName != '' && FloatStats[i].LinkedVariableName != 'None')
          {
            ForEach LinkedVariables(class'SeqVar_Float', SeqVar_Float)
            {
              if (SeqVar_Float.VarName == FloatStats[i].LinkedVariableName)
              {
                switch (ModifyMethod)
                {              
                case MM_Add:
                  OnlineStatsWrite.SetFloatStat(FloatStats[i].StatId, ReadFloatStatValue + SeqVar_Float.FloatValue);
                  break;

                case MM_Subtract:
                  OnlineStatsWrite.SetFloatStat(FloatStats[i].StatId, ReadFloatStatValue - SeqVar_Float.FloatValue);
                  break;

                default:
                  break;
                }
              }
            }
          }
          // Otherwise use the value defined in the struct
          else
          {
            switch (ModifyMethod)
            {              
            case MM_Add:
              OnlineStatsWrite.SetFloatStat(FloatStats[i].StatId, ReadFloatStatValue + FloatStats[i].Value);
              break;

            case MM_Subtract:
              OnlineStatsWrite.SetFloatStat(FloatStats[i].StatId, ReadFloatStatValue - FloatStats[i].Value);
              break;

            default:
              break;
            }
          }
        }

        // Send the write request to the stat handler      
        if (OnlineSubsystem.StatsInterface.WriteOnlineStats(SessionName, ProcessingPlayers[0].LocalNetId, OnlineStatsWrite))
        {
          // Add the flush delegate
          OnlineSubsystem.StatsInterface.AddFlushOnlineStatsCompleteDelegate(InternalOnFlushOnlineStatsComplete);
          // Flush the online stats
          OnlineSubsystem.StatsInterface.FlushOnlineStats(SessionName);
        }
        // Activate the failed output link
        else
        {
          ForceActivateOutput(3);
        }
      }
    }
  }
  else
  {
    FinishedProcessPlayerIndex(bWasSuccessful);
  }
}
`endif

/**
 * Called when this Kismet node should just set the stats value
 */
protected function SetStatValue()
{
  local OnlineSubsystem OnlineSubsystem;  
  local int i;
  local SeqVar_Int SeqVar_Int;  
  local SeqVar_Float SeqVar_Float;

  // Write stats
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.StatsInterface != None)
  {    
    // Instance the stats write object
    OnlineStatsWrite = new () OnlineStatsWriteClass;
    if (OnlineStatsWrite != None)
    {
      // Set the int stats write values
      for (i = 0; i < IntStats.Length; ++i)
      {
        // If this state has a variable name, then perform a look up
        if (IntStats[i].LinkedVariableName != '' && IntStats[i].LinkedVariableName != 'None')
        {
          ForEach LinkedVariables(class'SeqVar_Int', SeqVar_Int)
          {
            if (SeqVar_Int.VarName == IntStats[i].LinkedVariableName)
            {
              OnlineStatsWrite.SetIntStat(IntStats[i].StatId, SeqVar_Int.IntValue);
              break;
            }
          }
        }
        // Otherwise use the value defined in the struct
        else
        {
          OnlineStatsWrite.SetIntStat(IntStats[i].StatId, IntStats[i].Value);
          break;
        }
      }

      // Modify the float stats write values
      for (i = 0; i < FloatStats.Length; ++i)
      {
        // If this state has a variable name, then perform a look up
        if (FloatStats[i].LinkedVariableName != '' && FloatStats[i].LinkedVariableName != 'None')
        {
          ForEach LinkedVariables(class'SeqVar_Float', SeqVar_Float)
          {
            if (SeqVar_Float.VarName == FloatStats[i].LinkedVariableName)
            {
              OnlineStatsWrite.SetFloatStat(FloatStats[i].StatId, SeqVar_Float.FloatValue);
            }
          }
        }
        // Otherwise use the value defined in the struct
        else
        {
          OnlineStatsWrite.SetFloatStat(FloatStats[i].StatId, FloatStats[i].Value);
        }
      }

      // Send the write request to the stat handler    
      if (OnlineSubsystem.StatsInterface.WriteOnlineStats(SessionName, ProcessingPlayers[0].LocalNetId, OnlineStatsWrite))
      {
// ==========
// SteamWorks
// ==========
`if(`isdefined(USE_STEAMWORKS))
        // Add the flush delegates
        OnlineSubsystem.StatsInterface.AddFlushOnlineStatsCompleteDelegate(InternalOnFlushOnlineStatsComplete);
`endif
        // Flush the online stats
        OnlineSubsystem.StatsInterface.FlushOnlineStats(SessionName);
// ==========
// GameCenter
// ==========
`if(`isdefined(USE_GAMECENTER))
        // Clear all object refs
        OnlineStatsWrite = None;
        OnlineStatsRead = None;

        // Handle process player index
        FinishedProcessPlayerIndex(true);
`endif
      }
      // Activate the failed output link
      else
      {
        ForceActivateOutput(3);
      }
    }
  }
}

`if(`isdefined(USE_STEAMWORKS))
/**
 * Called when the stats flush operation has completed
 *
 * @param SessionName the name of the session having stats flushed for
 * @param bWasSuccessful true if the async action completed without error, false if there was an error
 */
function InternalOnFlushOnlineStatsComplete(Name InSessionName, bool bWasSuccessful)
{
  // Clear all object refs
  OnlineStatsWrite = None;
  OnlineStatsRead = None;

  // Handle process player index
  FinishedProcessPlayerIndex(bWasSuccessful);
}
`endif

/**
 * Called when this Kismet node should clean up all variable or delegate references so that garbage collection can occur
 *
 * @param    IsExiting      If exiting, then clean up everything
 */
function CleanUp(optional bool IsExiting)
{
// ==========
// SteamWorks
// ==========
`if(`isdefined(USE_STEAMWORKS))
  local OnlineSubsystem OnlineSubsystem;
  local int i, InPlayerIndex;

  // Grab the online subsystem and remove all delegate binds
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.StatsInterface != None)
  {
    // There can only be 4 maximum split screen local players
    for (i = 0; i < 4; ++i)
    {
      // Get the player index
      InPlayerIndex = class'UIInteraction'.static.GetPlayerIndex(i);
      if (InPlayerIndex >= 0)
      {
        // Clear the delegates
        OnlineSubsystem.StatsInterface.ClearFlushOnlineStatsCompleteDelegate(InternalOnFlushOnlineStatsComplete);
        OnlineSubsystem.StatsInterface.ClearReadOnlineStatsCompleteDelegate(InternalOnReadOnlineStatsComplete);
      }
    }
  }
`endif

  // Clear the online stats write object
  OnlineStatsWrite = None;
  // Clear the online stats read object
  OnlineStatsRead = None;

  // Call the super just in case we are exiting
  Super.CleanUp(IsExiting);
}

defaultproperties
{
  SessionName="Game"
  ObjName="Modify Online Stat"  

  VariableLinks(1)=(ExpectedType=class'SeqVar_Int',LinkDesc="Stat Values")
}

Example Kismet

KismetOnlineSubsystem_11.png


Steam Works only Kismet Nodes


SeqAct_RefreshAchievements

This Kismet Sequence Action attempts to force the client to refresh it's achievements by reading them from Steamworks.

KismetOnlineSubsystem_02.png

Functions

  • InternalOnActivated - Which this function is called, it binds to a delegate and calls OnlineSubsystem.PlayerInterface.ReadAchievements().
  • InternalOnReadAchievementsComplete - This function called when OnlineSubsystem.PlayerInterface.ReadAchievements() has finished reading achievements from Steamworks. It calls FinishedProcessPlayerIndex().
  • CleanUp - This function cleans up any delegates that have been bound.

Execution flow

When InternalOnActivated() is called, it first binds to a delegate in OnlineSubsystem.PlayerInterface. This delegate is called when the online subsystem has finished reading the achievements from Steamworks. OnlineSubsystem.PlayerInterface.ReadAchievements() is then called. When reading achievements have finished, InternalOnReadAchievementsComplete() is then called, which then calls FinishedProcessPlayerIndex().

When CleanUp() is called, any delegates that have been bound are freed.

Unrealscript

SeqAct_RefreshAchievements.uc
/**
 * A Refresh Achievements Action is used to refresh all achievements.
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_RefreshAchievements extends SeqAct_OnlineSubsystemBase
`if(`notdefined(USE_STEAMWORKS))
  abstract
  HideDropDown;
`else
  ;

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  local OnlineSubSystem OnlineSubSystem;

  OnlineSubSystem = class'GameEngine'.static.GetOnlineSubSystem();
  if (OnlineSubSystem != None && OnlineSubSystem.PlayerInterface != None)
  {
    // Bind the delegate
    OnlineSubSystem.PlayerInterface.AddReadAchievementsCompleteDelegate(ProcessingPlayers[0].LocalUserNum, InternalOnReadAchievementsComplete);

    // Read all achievements 
    OnlineSubsystem.PlayerInterface.ReadAchievements(ProcessingPlayers[0].LocalUserNum);
  }
}

/**
 * Called when the async achievements read has completed
 *
 * @param TitleId the title id that the read was for (0 means current title)
 */
function InternalOnReadAchievementsComplete(int TitleId)
{
  FinishedProcessPlayerIndex(true);
}

/**
 * Called when this Kismet node should clean up all variable or delegate references so that garbage collection can occur
 *
 * @param    IsExiting      If exiting, then clean up everything
 */
function CleanUp(optional bool IsExiting)
{
  local OnlineSubsystem OnlineSubsystem;
  local int i, InPlayerIndex;

  // Grab the online subsystem and remove all delegate binds
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.PlayerInterface != None)
  {
    // There can only be 4 maximum split screen local players
    for (i = 0; i < 4; ++i)
    {
      // Get the player index
      InPlayerIndex = class'UIInteraction'.static.GetPlayerIndex(i);
      if (InPlayerIndex >= 0)
      {
        // Clear the delegates
        OnlineSubsystem.PlayerInterface.ClearReadAchievementsCompleteDelegate(InPlayerIndex, InternalOnReadAchievementsComplete);
      }
    }
  }

  // Call the super just in case we are exiting
  Super.CleanUp(IsExiting);
}
`endif

defaultproperties
{
  ObjName="Refresh Achievements"
}


SeqAct_ResetAchievements

This Kismet Sequence Action is used to reset achievements and or stats that has been saved for the player. This Kismet Sequence Action should only be used for debugging purposes as it binds directly to the Steamworks online subsystem.

KismetOnlineSubsystem_03.png

Functions

  • InternalOnActivated - When this is called this calls OnlineSubsystemSteamworks.ResetStats().

Definitions

  • ResetAchievements - If this is true, then achievements are reset as well.

Execution flow

When InternalOnActivated() is called, it then calls OnlineSubsystemSteamworks.ResetStats(). It then calls FinishedProcessPlayerIndex().

Unrealscript

SeqAct_ResetAchievements.uc
/**
 * A Reset Achievements Action is used to reset all of the achievements earnt by the player.
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_ResetAchievements extends SeqAct_OnlineSubsystemBase
`if(`notdefined(USE_STEAMWORKS))
  abstract
  HideDropDown;
`else
  ;

// If true, then achievements are also reset. Otherwise only stats are reset.
var() const bool ResetAchievements;

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  local OnlineSubsystemSteamworks OnlineSubsystemSteamworks;
  local bool bWasSuccessful;

  OnlineSubsystemSteamworks = OnlineSubsystemSteamworks(class'GameEngine'.static.GetOnlineSubsystem());
  if (OnlineSubsystemSteamworks != None)
  {
    bWasSuccessful = OnlineSubsystemSteamworks.ResetStats(ResetAchievements);
  }
  else
  {
    bWasSuccessful = false;
  }

  FinishedProcessPlayerIndex(bWasSuccessful);
}
`endif

defaultproperties
{
`if(`isdefined(USE_STEAMWORKS))
  ResetAchievements=true
`endif
  ObjName="Reset Achievements/Stats"
}

Example Kismet

KismetOnlineSubsystem_12.png


SeqAct_ReadOnlineStat

This Kismet Sequence Action allows you to read online stats to use within Kismet later on.

KismetOnlineSubsystem_04.png

KismetOnlineSubsystem_09.jpg

Functions

  • InternalOnActivated - This is called by the parent class, SeqAct_OnlineSubsystemBase. This then starts the call to read the stats from Steamworks.
  • InternalOnReadOnlineStatsComplete - This is called when reading the stats from Steamworks has finished. It finally calls FinishedProcessPlayerIndex().
  • CleanUp - This function cleans up any delegates and object references that is used by this Kismet Sequence Action.

Definitions

  • EReadStatsMethod - An enum which defines the different ways to read stats.
  • SLinkedVariableName - A struct which holds the an id for the stat you want to read, and a name which links to an attached Kismet Variable.
  • OnlineStatsReadClass - Online Read Stats class to use for reading online stats.
  • ReadStatsMethod - Variable declaration of EReadStatsMethod.
  • StartRankIndex - If use a read stats ranked method, which starting rank index to use.
  • RowCount - How many rows to read if using read stats rank or rank around player methods.
  • LinkedVariableNames - Linked variables to output to.
  • Rank - Ranked variable that is mapped to a Kismet Variable.

Execution flow

When InternalOnActivated() is called, it first instances an OnlineStatsRead based on the class defined in OnlineStatsReadClass. OnlineSubsystem.StatsInterface.FreeStats() is then called to clear the OnlineStatsRead instance. A delegate is then bound and the request to read the online stats is done. The method to read the online stats is defined by the ReadStatsMethod variable. This may call OnlineSubsystem.StatsInterface.ReadOnlineStatsForFriends(), OnlineSubsystem.StatsInterface.ReadOnlineStatsByRank(), OnlineSubsystem.StatsInterface.ReadOnlineStatsByRankAroundPlayer() or OnlineSubsystem.StatsInterface.ReadOnlineStats(). InternalOnReadOnlineStatsComplete() is called when the online stats has finished, this function is responsible for getting the stats and outputting them into the attached Kismet Sequence Variables. Rank is also set here too. PopulateLinkedVariableValues() is called to copy the mapped stat properties across. FinishedProcessPlayerIndex() is then called.

When CleanUp() is called, all delegates that have been bound and object instances are freed.

Unrealscript

SeqAct_ReadOnlineStat.uc
/**
 * A Read Online Stat Action is used to read online stats.
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_ReadOnlineStat extends SeqAct_OnlineSubsystemBase
// ==========
// GameCenter
// ==========
`if(`isdefined(USE_GAMECENTER))
  abstract
  HideDropdown;
`else
  ;
//
enum EReadStatsMethod
{
  RST_ReadAll<DisplayName=Read all stats>,
  RST_ReadFriendsOnly<DisplayName=Read stats of friends>,
  RST_ReadByRank<DisplayName=Read stats by rank>,
  RST_ReadByRankAroundPlayer<DisplayName=Read stats around player>
};

//
struct SLinkedVariableName
{
  var() int StatId;
  var() Name LinkedVariableName;
};

// Class to use which reads the stats
var() class<OnlineStatsRead> OnlineStatsReadClass;
// Method to read stats
var() EReadStatsMethod ReadStatsMethod;
// If use a read stats ranked method, which starting rank index to use
var() int StartRankIndex;
// How many rows to read if using read stats rank or rank around player methods
var() int RowCount;
// Linked variables to output to
var() array<SLinkedVariableName> LinkedVariableNames;

// Online stats read object instance
var protected OnlineStatsRead OnlineStatsRead;
// Rank variable mapped to a Kismet variable
var int Rank;

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  local OnlineSubSystem OnlineSubSystem;
  local array<UniqueNetId> PlayerIds;

  // Abort if the online stats read class is none
  // Abort if there are no column ids
  if (OnlineStatsReadClass == None || LinkedVariableNames.Length <= 0)
  {
    return;
  }
  
  // Request stats
  OnlineSubSystem = class'GameEngine'.static.GetOnlineSubSystem();
  if (OnlineSubSystem != None && OnlineSubSystem.StatsInterface != None)
  {    
    // Instance the online stats
    OnlineStatsRead = new () OnlineStatsReadClass;
    if (OnlineStatsRead != None)
    {
      // Free the stats
      OnlineSubsystem.StatsInterface.FreeStats(OnlineStatsRead);

      // Bind the read online stats delegate
      OnlineSubsystem.StatsInterface.AddReadOnlineStatsCompleteDelegate(InternalOnReadOnlineStatsComplete);

      switch (ReadStatsMethod)
      {          
      // Read stats for friends only
      case RST_ReadFriendsOnly:
        OnlineSubsystem.StatsInterface.ReadOnlineStatsForFriends(ProcessingPlayers[0].LocalUserNum, OnlineStatsRead);
        break;

      // Read stats by rank only
      case RST_ReadByRank:
        OnlineSubsystem.StatsInterface.ReadOnlineStatsByRank(OnlineStatsRead, StartRankIndex, RowCount);
        break;

      // Read stats around player
      case RST_ReadByRankAroundPlayer:
        OnlineSubsystem.StatsInterface.ReadOnlineStatsByRankAroundPlayer(ProcessingPlayers[0].LocalUserNum, OnlineStatsRead, RowCount);
        break;

        // Read all stats
      case RST_ReadAll:
      default:
        PlayerIds.AddItem(ProcessingPlayers[0].LocalNetId);
        OnlineSubsystem.StatsInterface.ReadOnlineStats(PlayerIds, OnlineStatsRead);
        break;
      }
    }
  }
}

/**
 * Notifies the interested party that the last stats read has completed
 *
 * @param bWasSuccessful true if the async action completed without error, false if there was an error
 */
function InternalOnReadOnlineStatsComplete(bool bWasSuccessful)
{
  local int i;
  local SeqVar_Int SeqVar_Int;
  local SeqVar_Float SeqVar_Float;

  if (OnlineStatsRead != None)
  {
    // Output the stat id for the processing player
    if (LinkedVariableNames.Length > 0)
    {
      for (i = 0; i < LinkedVariableNames.Length; ++i)
      {
        if (LinkedVariableNames[i].LinkedVariableName != '' && LinkedVariableNames[i].LinkedVariableName != 'None')
        {
          // Copy the int stats over to the sequence variable
          ForEach LinkedVariables(class'SeqVar_Int', SeqVar_Int)
          {
            if (SeqVar_Int.VarName == LinkedVariableNames[i].LinkedVariableName)
            {
              OnlineStatsRead.GetIntStatValueForPlayer(ProcessingPlayers[0].LocalNetId, LinkedVariableNames[i].StatId, SeqVar_Int.IntValue);
            }
          }

          // Copy the float stats over to the sequence variable
          ForEach LinkedVariables(class'SeqVar_Float', SeqVar_Float)
          {
            if (SeqVar_Float.VarName == LinkedVariableNames[i].LinkedVariableName)
            {
              OnlineStatsRead.GetFloatStatValueForPlayer(ProcessingPlayers[0].LocalNetId, LinkedVariableNames[i].StatId, SeqVar_Float.FloatValue);
            }
          }
        }
      }
    }

    // Output rank
    Rank = OnlineStatsRead.GetRankForPlayer(ProcessingPlayers[0].LocalNetId);
    // Populate the linked variables
    PopulateLinkedVariableValues();
  }

  // Finished processing players
  FinishedProcessPlayerIndex(bWasSuccessful);
  // Remove reference to online stats read 
  OnlineStatsRead = None;
}

/**
 * Called when this Kismet node should clean up all variable or delegate references so that garbage collection can occur
 *
 * @param    IsExiting      If exiting, then clean up everything
 */
function CleanUp(optional bool IsExiting)
{
  local OnlineSubsystem OnlineSubsystem;
  local int i, InPlayerIndex;

  // Grab the online subsystem and remove all delegate binds
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem != None && OnlineSubsystem.StatsInterface != None)
  {
    // There can only be 4 maximum split screen local players
    for (i = 0; i < 4; ++i)
    {
      // Get the player index
      InPlayerIndex = class'UIInteraction'.static.GetPlayerIndex(i);
      if (InPlayerIndex >= 0)
      {
        // Clear the delegates
        OnlineSubsystem.StatsInterface.ClearReadOnlineStatsCompleteDelegate(InternalOnReadOnlineStatsComplete);
      }
    }
  }

  // Clear the online stats read object
  OnlineStatsRead = None;

  // Call the super just in case we are exiting
  Super.CleanUp(IsExiting);
}
`endif

defaultproperties
{
  ObjName="Read Online Stat"

// ==========
// SteamWorks
// ==========
`if(`isdefined(USE_STEAMWORKS))
  StartRankIndex=1
  RowCount=50
`endif

  VariableLinks(1)=(ExpectedType=class'SeqVar_Int',LinkDesc="Stat Values",bWriteable=true)
  VariableLinks(2)=(ExpectedType=class'SeqVar_Int',LinkDesc="Rank",bWriteable=true,PropertyName=Rank)
}

Example Kismet

KismetOnlineSubsystem_13.png


Game Center only Kismet Nodes


SeqAct_Mobile_ShowAchievementsUI

When this Kismet Sequence Action is activated, it brings up Game Center's Achievements UI. This normally automatically pauses the game.

KismetOnlineSubsystem_05.png

Functions

  • InternalOnActivated - When this function is called, it calls OnlineSubSystem.PlayerInterfaceEx.ShowAchievementsUI().

Execution flow

When InternalOnActivated() is called by the parent class, SeqAct_OnlineSubsystemBase, it then calls OnlineSubSystem.PlayerInterfaceEx.ShowAchievementsUI(). It then calls FinishedProcessPlayerIndex().

Unrealscript

SeqAct_Mobile_ShowAchievementsUI.uc
/**
 * A Mobile Show Achievements UI Action is used to display the achievements UI.
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_Mobile_ShowAchievementsUI extends SeqAct_OnlineSubsystemBase
`if(`notdefined(USE_GAMECENTER))  
  abstract
  HideDropDown;
`else
  ;

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  local OnlineSubsystem OnlineSubsystem;

  // Get the online sub system
  OnlineSubSystem = class'GameEngine'.static.GetOnlineSubsystem();
  // Check that the online subsystem and player interface ex is accessible
  if (OnlineSubSystem != None && OnlineSubSystem.PlayerInterfaceEx != None)
  {
    OnlineSubSystem.PlayerInterfaceEx.ShowAchievementsUI(ProcessingPlayers[0].LocalUserNum);
    FinishedProcessPlayerIndex(true);
  }  
  else
  {
    FinishedProcessPlayerIndex(false);
  }
}
`endif

defaultproperties
{
  ObjName="Show Achievements UI"
}

Example Kismet

KismetOnlineSubsystem_14.png


SeqAct_Mobile_ShowLeaderboardsUI

When this Kismet Sequence Action is activated, it brings up the Game Center's Leaderboards UI. This normal automatically pauses the game.

KismetOnlineSubsystem_06.png

Functions

  • InternalOnActivated - When this function is called, it creates the instance of OnlineStatsRead based on the OnlineStatsReadClass value. OnlineSuppliedUIInterface.ShowOnlineStatsUI() is then called to bring up Game Center's leaderboards UI. A delegate is also bound which calls InternalOnShowOnlineStatsUIComplete when the UI is closed.
  • InternalOnShowOnlineStatsUIComplete - This function called when the Leaderboards UI is closed.
  • CleanUp - This function is called to free up all bound delegates and object references.

Definitions

  • OnlineStatsReadClass - The class of the OnlineStatsRead you want to use to read the online stats.

Execution flow

When InternalOnActivated() is called by the parent class, SeqAct_OnlineSubsystemBase, it instances the OnlineReadStats defined by OnlineStatsReadClass. A delegate is bound to detect when the leaderboards UI is closed. OnlineSuppliedUIInterface.ShowOnlineStatsUI() is then called to read and show the leaderboards UI. FinishedProcessPlayerIndex() is then called.

When InternalOnShowOnlineStatsUIComplete() is called, the "Closed" output is activated.

When CleanUp() is called, all bound delegates are freed.

Unrealscript

SeqAct_Mobile_ShowLeaderboardsUI.uc
/**
 * A Mobile Show Leaderboards UI Action is used to display the leaderboards UI.
 *
 * Copyright 1998-2012 Epic Games, Inc. All Rights Reserved.
 */
class SeqAct_Mobile_ShowLeaderboardsUI extends SeqAct_OnlineSubsystemBase
`if(`notdefined(USE_GAMECENTER))
  abstract
  HideDropDown;
`else
  ;

var() const class<OnlineStatsRead> OnlineStatsReadClass;

/**
 * Called when this sequence action should do something attached with a local user num
 */
protected function InternalOnActivated()
{
  local OnlineSubsystem OnlineSubsystem;
  local OnlineSuppliedUIInterface OnlineSuppliedUIInterface;
  local array<UniqueNetId> PlayerIds;
  local OnlineStatsRead OnlineStatsRead;
  local int i;

  // If this class is none, then abort 
  if (OnlineStatsReadClass == None)
  {
    return;
  }

  // Get the online sub system
  OnlineSubSystem = class'GameEngine'.static.GetOnlineSubsystem();
  // Check that the online subsystem is accessible
  if (OnlineSubSystem != None)
  {
    // Create the PlayerIds array from all the processing players array
    for (i = 0; i < ProcessingPlayers.Length; ++i)
    {
      PlayerIds.AddItem(ProcessingPlayers[i].LocalNetId);
    }

    // Get the online supplied UI interface
    OnlineSuppliedUIInterface = OnlineSuppliedUIInterface(OnlineSubSystem.GetNamedInterface('SuppliedUI'));
    if (OnlineSuppliedUIInterface != None)
    {
      // Instance the online stats read class
      OnlineStatsRead = new () OnlineStatsReadClass;
      if (OnlineStatsRead != None)
      {
        // Bind to the online stats UI delegate
        OnlineSuppliedUIInterface.AddShowOnlineStatsUICompleteDelegate(InternalOnShowOnlineStatsUIComplete);

        // Show the online stats UI
        OnlineSuppliedUIInterface.ShowOnlineStatsUI(PlayerIds, OnlineStatsRead);
        FinishedProcessPlayerIndex(true, true);
      }
    }
  }
  else
  {
    FinishedProcessPlayerIndex(false);
  }
}

/**
 * Delegate fired when the supplied stats UI is closed
 */
function InternalOnShowOnlineStatsUIComplete()
{
  // Activate the fourth output
  ForceActivateOutput(4);
}

/**
 * Called when this Kismet node should clean up all variable or delegate references so that garbage collection can occur
 *
 * @param    IsExiting      If exiting, then clean up everything
 */
function CleanUp(optional bool IsExiting)
{
  local OnlineSubsystem OnlineSubsystem;
  local OnlineSuppliedUIInterface OnlineSuppliedUIInterface;

  Super.CleanUp(IsExiting);

  if (IsExiting)
  {
    // Get the online sub system
    OnlineSubSystem = class'GameEngine'.static.GetOnlineSubsystem();
    // Check that the online subsystem is accessible
    if (OnlineSubSystem != None)
    {
      // Get the online supplied UI interface
      OnlineSuppliedUIInterface = OnlineSuppliedUIInterface(OnlineSubSystem.GetNamedInterface('SuppliedUI'));
      if (OnlineSuppliedUIInterface != None)
      {
        OnlineSuppliedUIInterface.ClearShowOnlineStatsUICompleteDelegate(InternalOnShowOnlineStatsUIComplete);
      }
    }
  }
}
`endif

defaultproperties
{
  ObjName="Show Leaderboards UI"

  OutputLinks(4)=(LinkDesc="Closed")
}

Example Kismet

KismetOnlineSubsystem_15.png

Downloads