UDN
Search public documentation:

GameCenterCH
English Translation
日本語訳
한국어

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主页 > 移动设备主页>虚幻引擎3: 移动设备概述 > 虚幻引擎 3: Apple iOS概述 > Game Center(游戏中心)
UE3 主页 > 复制 > 游戏中心

Game Center(游戏中心)


概述


Game Center是Apple的在线游戏网络。它使得游戏玩家通过iOS设备连接到其上进行成就排行计较及竞赛。使用虚幻引擎3开发的iOS设备的游戏支持Game Center的应用,使得这些游戏具有非常好的社交性游戏体验。

ALERT! 注意: 在以下部分将使用GC指代Game Center。
ALERT! 注意: 重点是 不要 直接在Unrealscript引用 OnlineSubsystemGameCenter,因为这样在您尝试在PC上启动UDK时将会出现问题。始终使用 OnlineSubsystem 引用,并且针对不同的平台,虚幻引擎 3 会自动使用正确的 OnlineSubsystem 类型。

配置设置


UE3通过OnlineSubsystemGameCenter 类来在iOS上支持GC。但是,因为它在启动过程中自动地弹出Game Center 登录/欢迎回来 屏幕。所以GC可以通过配置文件进行启用和禁用。允许开发人员在开发过程中不需要进行 GC 测试的时候关闭它,只需将它设置为 true 即可。否则,将它设置为 false。

.\UDKGame\Config\IPhone\IPhoneEngine.ini
[OnlineSubsystemGameCenter.OnlineSubsystemGameCenter]
bDisableGameCenter=false

如果您打算使用成就或排行榜,那么您将需要给所有的成就和排行榜标识符设置前缀。Apple使用全局唯一的成就和排行榜字符串名称来标识它们,所以您应该使用您的唯一的 App/Bundle 标识符,并使用它作为基础:

UDKGame\Config\IPhone\IPhoneEngine.ini
[OnlineSubsystemGameCenter.OnlineSubsystemGameCenter]
bDisableGameCenter=false
UniqueAchievementPrefix=com.epicgames.exploreue3.achievement_
UniqueCategoryPrefix=com.epicgames.exploreue3.leaderboard_
EpicUniqueAchievementPrefix=com.epicgames.exploreue3.achievement_
EpicUniqueCategoryPrefix=com.epicgames.exploreue3.leaderboard_

当您使用iTunes Connect 来创建成就和排行榜类别时,您必须使用您指定的任何前缀开头来命名它们,然后以此类推地为每个名称附加01、02、03等。(当您解除锁定achievement 1,那么代码将会附加01到您的UniqueAchievementPrefix上)。如果是 UDK 用户,您必须同时设置 Unique 和 Epic 成就前缀和类别前缀。

编译 OnlineSubsystemGameCenter


在对您的 iOS 游戏使用 Game Center 之前,您可能需要编译 OnlineSubsystemGameCenter。要编译 OnlineSubsystemGameCenter,您需要将其添加给 DefaultEngine.ini 中 EditPackages 数组。

UDKGame\Config\DefaultEngine.ini
[UnrealEd.EditorEngine]
+EditPackages=UTGame
+EditPackages=UTGameContent
+EditPackages=OnlineSubsystemGameCenter

添加到 EditPackages 列表后,您将需要重新编译您的Unrealscript源代码。OnlineSubsystemGameCenter 包现在应该会显示在 UDKGame\Script 文件夹中。点击这里了解更多有关编译 Unrealscript 的信息。

成就


Achievement.png

成就的处理方式和其他平台类似,但当您第一次解除锁定一个成就时GC不会在屏幕上显示信息。当您的游戏代码试图解除锁定一个成就时,它应该首先检查确保它还没有被其他用户解锁。这是您可以使用的一个基本流程:

  • 游戏启动 (它仅需要做一次,但是多次操作也可以)。
    • 调用在完成阅读成就数据后会通知的 OnlineSub.PlayerInterface.AddReadAchievementsCompleteDelegate()
    • 调用 OnlineSub.PlayerInterface.ReadAchievements() 来读取成绩。
    • 注意,在您的代理中已经读取了成绩,所以现在您可以查询它们的状态。
  • 玩家正在玩游戏,满足了成就标准。
    • 调用 OnlineSub.PlayerInterface.GetAchievements() 来获得所有成就的状态。
    • 在返回的数组中查找匹配的成就ID。
      • 不要 使用您的成就ID作为成就数组的索引! 首先,成就 ID 会使用 1 作为它们的数组开始索引(Unrealscript及很多其他语言会使用 0 作为数组的开始索引)。
    • 如果 bWasAchievedOnline 为 false;
      • 那么显示您的UI、播放声音等。
      • 调用 OnlineSub.PlayerInterface.UnlockAchievement() 告诉GC玩家已经解除了对该成就的锁定。

为了使玩家看到他们已经解除了对哪些成就的锁定,您可以或者使用您自己的UI(通过使用 OnlineSub.PlayerInterface.GetAchievements() 的结果),或者显示GC内置屏幕。要想完成这个处理,简单地调用 OnlineSub.PlayerInterfaceEx.ShowAchievementsUI() 方法,GC UI就会滑动到屏幕中。

成就技术细节

当GC代码启动时,它将会立即开始下载成就,以便当您的游戏代码运行时,它们已经下载完成。但是,保险起见,请使用具有代理的 OnlineSub.PlayerInterface.ReadAchievements() 函数调用,以便确保成就下载已经完成,并且那个 OnlineSub.PlayerInterface.GetAchievements() 将返回真正的有效的结果。

在底层,针对成就有很多复杂的处理,因为如果用户在成就解锁时候下线,GC将不会通知服务器。所以,我们维持了一个本地的成就状态,它被保存到iOS闪存盘中。在这之后无论何时用户连接到GC时,引擎将会检查远程成就状态和本地成就状态,并将二者融合,通过在不显示UI的情况下解锁成就来更新远程服务器端的信息。

ALERT! 注意: 如果成就曾将将它们的 ID 设置为 -1,那么说明成就下载错误,UniqueAchievementPrefix 和/或 EpicUniqueAchievementPrefix 设置不正确。

成就示例

在这个示例中,创建成就处理器 actor 类允许您将成就调用传递给它。当然您可以将它转换为其他类,例如您的自定义 GameInfo 类。成就控制器的工作方式是存储等待处理的成就列表(以防连续快速地获得某些成就)然后调用解锁所有等待处理的成就之前将会在它们之间循环的异步函数。

要解锁一个成就,请将这个成就 id 作为一个参数调用 YourAchievementHandler::UnlockAchievement()。这个成就 id 必须与成就 id 末尾的数值匹配。例如,id 为 com.epicgames.exploreue3.achievement_01 的成就将会有一个 id 为 1 的成就。记住成就 id 通常都是从 1 开始的,而不是 0。

第一个检查是为了确保成就 id 在等待处理的成就数组中。这样可以防止成就被多次解锁。如果成就处理器目前没有处理任何成就,那么会开始处理等待处理的成就,同时将 ProcessingAchievements 标记设置为 true。这是为了开始异步成就处理循环。

由此,已经完成分配在从服务器中读取结束的时候调用的代理,同时进行了可以读取成就的异步调用。

YourAchievementHandler.uc
class YourAchievementHandler extends Actor;

// 等待处理的成就
var array<int> PendingAchievements;
// 如果我们目前正在处理成就,那么为 True
var bool ProcessingAchievements;

/**
 * 为玩家解锁成就
 *
 * @param    AchievementId      要解锁的成就
 * @param    LocalUserNum      本地用户索引
 */
function UnlockAchievement(int AchievementId)
{
  local OnlineSubsystem OnlineSubsystem;
  local int PlayerControllerId;

  // 这个成就已经处于等待处理状态中,并且正在进行中,所以请等待
  if (PendingAchievements.Find(AchievementId) != INDEX_NONE)
  {
    return;
  }

  // 将这个成就 id 添加到等待处理列表中
  PendingAchievements.AddItem(AchievementId);

  // 如果我们现在没有处理成就,那么马上就开始处理
  if (!ProcessingAchievements)
  {
    // 连接到 GameCenter 并链接成就代理
    OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
    if (OnlineSubsystem != None && OnlineSubsystem.PlayerInterface != None)
    {
      // 获取本地玩家控制器 id
      PlayerControllerId = GetALocalPlayerControllerId();

      // 分配读取成就代理
      OnlineSubsystem.PlayerInterface.AddReadAchievementsCompleteDelegate(PlayerControllerId, InternalOnReadAchievementsComplete);

      // 读取所有成就
      OnlineSubsystem.PlayerInterface.ReadAchievements(PlayerControllerId);

      // 设置为 true,防止它再次被执行
      ProcessingAchievements = true;
    }
  }
}

下面是一个简单的辅助函数,它会返回本地玩家控制器 id。

YourAchievementHandler.uc
/**
 * 会返回本地玩家控制器 id。同样的规则适用于 Actor::GetALocalPlayerController()。
 *
 * @return    会返回本地玩家控制器 id
 */
function int GetALocalPlayerControllerId()
{
  local PlayerController PlayerController;
  local LocalPlayer LocalPlayer;

  // 获取本地玩家控制器
  PlayerController = GetALocalPlayerController();
  if (PlayerController == None)
  {
    return INDEX_NONE;
  }

  // 获取本地玩家信息
  LocalPlayer = LocalPlayer(PlayerController.Player);
  if (LocalPlayer == None)
  {
    return INDEX_NONE;
  }

  return class'UIInteraction'.static.GetPlayerIndex(LocalPlayer.ControllerId);
}

在从 Game Center 中读取成就数据后,会调用 YourAchievementHandler::InternalOnReadAchievementsComplete()。首先清除 DownloadedAchievements 数组确保它里面没有数据,然后调用 PlayerInterface::GetAchievements() 将最新下载的成就数据填入 DownloadedAchievements 数组。

至此,我们可以将第一个等待处理的成就 id 作为密钥执行一个快速的数组索引搜索。如果成就在线没有实现,那么将会设置在解锁成就的时候调用的代理,然后调用 PlayerInterface::UnlockAchievement()。它会通过 Game Center 开始异步请求来解锁这个成就。然后会删除读取成就代理,这样垃圾回收才可以正确地进行。

YourAchievementHandler.uc
class YourAchievementHandler extends Actor;

// 其中包含所有下载成就的数组
var array<AchievementDetails> DownloadedAchievements;

/**
 * 在异步成就读取完成的时候进行调用
 *
 * @param    TitleId      这次读取的标题 id(0 表示当前标题)
 */
function InternalOnReadAchievementsComplete(int TitleId)
{
  local OnlineSubsystem OnlineSubsystem;
  local int AchievementIndex, PlayerControllerId;

  // 确保我们有一个在线子系统和一个相关联的玩家界面
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem == None || OnlineSubsystem.PlayerInterface == None)
  {
    return;
  }

  // 获取本地玩家控制器 id
  PlayerControllerId = GetALocalPlayerControllerId();

  // 清除当前下载的成就数组,与此同时复制新数据的时候
  DownloadedAchievements.Remove(0, DownloadedAchievements.Length);

  // 将成就读取到下载的成就数组中
  OnlineSubsystem.PlayerInterface.GetAchievements(PlayerControllerId, DownloadedAchievements, TitleId);

  // 获取多有成就
  if (DownloadedAchievements.Length > 0 && PendingAchievements.Length > 0)
  {
    // 获取成就索引
    AchievementIndex = DownloadedAchievements.Find('Id', PendingAchievements[0]);

    // 解锁成就
    if (AchievementIndex != INDEX_NONE && !DownloadedAchievements[AchievementIndex].bWasAchievedOnline)
    {
      // 分配解锁成就完整代理
      OnlineSubsystem.PlayerInterface.AddUnlockAchievementCompleteDelegate(PlayerControllerId, InternalOnUnlockAchievementComplete);

      // 开始解锁处理
      OnlineSubsystem.PlayerInterface.UnlockAchievement(PlayerControllerId, PendingAchievements[0]);
    }
  }

  // 删除这个代理引用,这样垃圾回收才可以进行
  OnlineSubsystem.PlayerInterface.ClearReadAchievementsCompleteDelegate(PlayerControllerId, InternalOnReadAchievementsComplete);
}

当 Game Center 回应说成就已经解锁的时候,将会调用 YourAchievementHandler::InternalOnUnlockAchievementComplete(),因为它在之前就已经分配给这个代理。在这里,您可以在图形用户界面上显示一些内容和/或播放一个声音来表示成就已经解锁。然后将这个成就 id 从 PendingAchievements 数组中赶出去。如果这个 PendingAchievements 数组仍然还有更多的成就 id 要进行解锁,那么再次进行循环处理。否则,会清理使用的代理并将 ProcessingAchievements 标记重新设置为 false。

YourAchievementHandler.uc
class YourAchievementHandler extends Actor;

/**
 * 在成就解锁完成的时候进行调用
 *
 * @param bWasSuccessful 如果一部操作完成,没有发生错误,那么为 true,如果有错误为 false
 */
function InternalOnUnlockAchievementComplete(bool bWasSuccessful)
{
  local OnlineSubsystem OnlineSubsystem;
  local PlayerController PlayerController;
  local int AchievementIndex, PlayerControllerId;

  // 获取本地玩家控制器 id
  PlayerControllerId = GetALocalPlayerControllerId();

  if (bWasSuccessful && PendingAchievements.Length > 0)
  {
    // 获取本地玩家控制器
    PlayerController = GetALocalPlayerController();
    if (PlayerController != None)
    {
      // 获取成就索引
      AchievementIndex = DownloadedAchievements.Find('Id', PendingAchievements[0]);

      // 将成就显示在用户界面播放器上
    }
  }

  // 弹出处理好的成就,不管它是否成功处理完成
  PendingAchievements.Remove(0, 1);

  // 确保我们有一个在线子系统和一个相关联的玩家界面
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem == None || OnlineSubsystem.PlayerInterface == None)
  {
    return;
  }

  // 如果我们还有等待处理的成就,那么处理下一个
  if (PendingAchievements.Length > 0)
  {
    // 连接到 GameCenter 并链接成就代理
    // 分配读取成就代理
    OnlineSubsystem.PlayerInterface.AddReadAchievementsCompleteDelegate(PlayerControllerId, InternalOnReadAchievementsComplete);

    // 读取所有成就
    OnlineSubsystem.PlayerInterface.ReadAchievements(PlayerControllerId);
  }
  else // 否则,我们就完蛋了,所以清除
  {
    // 清除代理绑定
    OnlineSubsystem.PlayerInterface.ClearUnlockAchievementCompleteDelegate(PlayerControllerId, InternalOnUnlockAchievementComplete);

    // 设置这个标记说明我们不再处理成就
    ProcessingAchievements = false;
  }
}

最后,要确保所有代理引用都已经清楚,这一点很重要;只有这样,垃圾回收可以正确进行。

YourAchievementHandler.uc
class YourAchievementHandler extends Actor;

/**
 * 当 actor 被销毁的时候进行调用
 */
event Destroyed()
{
  local OnlineSubsystem OnlineSubsystem;
  local int PlayerControllerId;

  Super.Destroyed();

  // 确保我们有一个在线子系统和一个相关联的玩家界面
  OnlineSubsystem = class'GameEngine'.static.GetOnlineSubsystem();
  if (OnlineSubsystem == None || OnlineSubsystem.PlayerInterface == None)
  {
    return;
  }

  // 如果我们还在处理成就,那么必须清楚分配的代理,这样才可以正常进行垃圾回收
  if (ProcessingAchievements)
  {
    // 获取本地玩家控制器 id
    PlayerControllerId = GetALocalPlayerControllerId();

    // 删除这个代理引用,这样垃圾回收才可以进行
    OnlineSubsystem.PlayerInterface.ClearReadAchievementsCompleteDelegate(PlayerControllerId, InternalOnReadAchievementsComplete);

    // 清除代理绑定
    OnlineSubsystem.PlayerInterface.ClearUnlockAchievementCompleteDelegate(PlayerControllerId, InternalOnUnlockAchievementComplete);
  }
}

排行榜


Leaderboard.png

GC的另一个主要的非-多玩家功能是排行榜。再次说明,排行榜的工作方式和其他平台上的该功能类似,但是GC排行榜有一些限制。GC实际上仅支持具有多个“类别”的一个“排行榜”。我们把这些类别当做一个排行榜表格中的一个列栏来对待。但是,所以类别都是使用相同的格式和标签来显示的(具有它们的UI),所以这些类别可以是时间、或整数点、或整数技能等。另外,它们或者是升序排列或者是降序排列。这将会影响您设置您的分数的方式。

要报告分数,请创建 OnlineStatsWrite 的一个子类,然后在默认属性块中设置它的属性(将 1 作为基础 ID)。在这里,您可以创建这个对象的一个实例,设置这些值,然后通过 OnlineSub.StatsInterface.WriteOnlineStats() 进行报告。

YourOnlineStatsWrite.uc
class YourOnlineStatsWrite extends OnlineStatsWrite;

const PROPERTY_KILLS = 1;
const PROPERTY_LEVEL = 2;
const PROPERTY_GOLD = 3;

defaultproperties
{
  Properties=((PropertyId=PROPERTY_KILLS,Data=(Type=SDT_Int32,Value1=0)),(PropertyId=PROPERTY_LEVEL,Data=(Type=SDT_Int32,Value1=0)),(PropertyId=PROPERTY_GOLD,Data=(Type=SDT_Int32,Value1=0)))
}

GC 不支持渐增的排行榜。如果您想要增加一个分数,例如杀的人,您将需要进行下面的操作:

  • 读取玩家所杀的人的保存值(您可以使用一个配置值或 BasicSaveObject/BasicLoadObject)
  • 相应地增加这个值
  • 将这个分数写到 Game Center

要显示分数,您可以通过调用下面的代码使用 GC 的内置 UI:

local OnlineSubsystem OnlineSubsystem;
local OnlineSuppliedUIInterface OnlineSuppliedUIInterface;
local array<UniqueNetId> PlayerIds;
local UniqueNetId PlayerId;
local YourOnlineStatsRead YourOnlineStatsRead;
local PlayerController PlayerController;
local LocalPlayer LocalPlayer;
local byte LocalUserNum;

// 获得在线子系统
OnlineSubSystem = class'GameEngine'.static.GetOnlineSubsystem();
// 检查在线子系统是否可以访问
if (OnlineSubSystem != None)
{
  // 通过本地玩家控制器创建 PlayerId 数组
  PlayerController = GetALocalPlayerController();
  if (PlayerController != None)
  {
    // 获取本地玩家
    LocalPlayer = LocalPlayer(PlayerController.Player);
    if (LocalPlayer != None)
    {
      // 获取本地用户数量
      LocalUserNum = class'UIInteraction'.static.GetPlayerIndex(LocalPlayer.ControllerId);
      // 从本地用户数获取唯一的玩家 id
      OnlineSubSystem.PlayerInterface.GetUniquePlayerId(LocalUserNum, PlayerId);
      // 将唯一的玩家 id 添加到玩家 id 数组
      PlayerIds.AddItem(PlayerId);

      // 获取在线提供的 UI 界面
      OnlineSuppliedUIInterface = OnlineSuppliedUIInterface(OnlineSubSystem.GetNamedInterface('SuppliedUI'));
      if (OnlineSuppliedUIInterface != None)
      {
        // 实例化在线统计数据并读取类
        YourOnlineStatsRead = new () class'YourOnlineStatsRead';
        if (YourOnlineStatsRead!= None)
        {
          // 现在在线统计数据 UI
          OnlineSuppliedUIInterface.ShowOnlineStatsUI(PlayerIds, YourOnlineStatsRead);
        }
      }
    }
  }
}

YourOnlineStatsRead.uc
class YourOnlineStatsRead extends OnlineStatsRead;

const PROPERTY_KILLS = 1;
const PROPERTY_LEVEL = 2;
const PROPERTY_GOLD = 3;

defaultproperties
{
  ColumnIds=(PROPERTY_KILLS,PROPERTY_LEVEL,PROPERTY_GOLD)
}

如果您读取了多个列栏的多个行,那么这些行将会基于第一列进行存储(这个稍后会改为使用分级栏)。

排行榜技术细节

和成就类似,如果在用户离线时报告了分数,那么GC将不能自动地重新提交它。所以,我们在底层做了很多处理来保存报告的最高和最低分数到磁盘中,然后我们在用户下次登录时把它们报告到服务器。我们不知道排行榜是升序的还是降序的,这也是为什么提交最高分和最低分的原因。

在读取统计数据的时候,所有分数都以 QWORDs / Int64s 形式进行报告(64 位整数,范围是 − 9,223,372,036,854,775,808 到 9,223,372,036,854,775,807),这种形式也就是 GC 存储排行榜值的方式。

多玩家


支持多玩家 - 目前仅是基于大厅的匹配比赛,最多4人。由于GC是通过 GKMatch 对象来处理网络流量的,所以通过GC匹配竞赛的玩家可以通过游戏网络进行通信。这4台iOS设备中的其中一个台将作为服务器。

Matchmaking(玩家匹配)

匹配玩家是通过GC的内置界面完成的:

   OnlineSuppliedUIInterface(OnlineSub.GetNamedInterface('SuppliedUI')).ShowMatchmakingUI();

ALERT! 注意: 目前传入给 ShowMatchmakingUI() 的GameSettings对象是没有用途的,可以为 None。

目前,没有办法选择谁是服务器 - 每个玩家都进入到匹配模式,然后选择其中一个设备作为服务器,其他设备就作为客户端(对于给定的玩家集合,服务器将总是同一个玩家 - 它使用每个玩家的唯一标示符来决定谁是服务武器,这样每个设备在不需要额外通信交流就可以知道它们是客户端还是服务器)。

根据玩家是被选作为服务器还是客户端,将会在匹配完成后调用两个代理其中的一个。如果是服务器端,那么将调用 OnCreateOnlineGameComplete() 代理。如果是客户端,那么将调用 OnJoinOnlineGameComplete() 代理。您应把这两个代理都注册到您的游戏中以便处理这两种情况。

在您的Join(客户端)代理中,您可以使用 OnlineSub.GameInterface.GetResolvedConnectString(SessionName,URL) 获得一个可以运行到服务器端的 url(通过 ClientTravel(URL, TRAVEL_Absolute) )。

ALERT! 要点: 当接收一个邀请时(请参照下面), PlayerController 具有添加一个Join代理的代码,然后会运行到那个URL。请参阅 PlayerController.uc 中的 OnInviteJoinComplete() 。由于这个原因,当用户已经选择进入到匹配模式时您仅需要注册一个Join代理。

在您的Create(服务器端)代理中,您一般要做类似于 * ClientTravel(SomeURL, TRAVEL_Absolute)* 的操作来加载地图。客户端将会获得服务器链接信息,并且将会通过打开URL连接加入服务器。如果这个用户进入到匹配模式或者如果它们接受了一个邀请,那么将使用这个代理,然后它将会被选作为服务器,所以您应该总是注册一个Create代理。

邀请

GC支持从玩家的匹配UI中邀请玩家。如果邀请的玩家正在玩另一个游戏,那么该玩家将会看到一个邀请信息,如果它们接受了邀请,那么它将会启动您的游戏,并且当游戏启动时,GC匹配UI将会滑出来向玩家显示他们正在加入这场比赛。

如果玩家已经在您的游戏中了,那么在 PlayerController (参照 OnGameInviteAccepted() )中的代码将会毁掉当前的任何在线游戏,并弹出GC匹配UI来向玩家显示它们正在加入的比赛。注意,被邀请的玩家实际上可能变为服务器。

语音聊天

GC有一个非常基本的、容易使用的语音聊天系统。当您进入到多人游戏后,您将会调用 OnlineSub.VoiceInterface.StartNetworkedVoice() 来让玩家彼此之间进行交谈。也可以通过使用标准的函数( MuteRemoteTalker() , IsRemotePlayerTalking() , 等)来静音和查询当前正在说话的人。

垃圾回收


每当您绑定到任何 Online Subsystem(在线子系统)代理时,确保它们始终会在关卡预计要进行垃圾回收的时候被清除,这一点很重要。它很重要,因为不这样做的话,垃圾回收将会失败,内存块将永远不会被释放! Kismet 在线子系统开发工具包精华文章会在以后深入地对它进行说明。

相关主题


  • Kismet 在线子系统 - 该开发工具包精华文章将会演示如何添加与 Game Center 相连接的 Kismet 节点。

下载