您的位置:首页 > 移动开发 > Unity3D

unity steamworksdk简单接入

2016-10-06 01:27 483 查看
VR越来越热,这个时间节点psvr还没正式发行,HTCvive属于VR设备里体验比较优秀的设备。开发vive应用上线主要有两个渠道,viveport官方商店及steam。两者官方都有详细的文档,但是上线steam平台,若是接入steamworks SDK可以提供更好的体验。steam官方提供的SDK都是C++代码,作为一个码农混子对C++只能一知半解。在选择sdk接入时找到别人用C#封装的原生steamworks SDK,官方链接:https://steamworks.github.io/。上述链接包含文档及sdk下载地址。接入时发现能查到的资料比较有限,自己在这尝试简单总结一下方便以后再次接入也希望能提供更容易理解的帮助。

导入SDK后主要脚本SteamManager,提供了Steamworks.NET的一些基础API供大家使用。

此SDK涵盖了原生steamworks提供的大部分功能,如:状态存储及成就,排行榜,用户授权,比赛安排,steam云等等功能,文档中都有详细的概述。使用过程中也发现他们很细心的使用了和C++相同的方法名来封装了C#的方法,这样在使用过程中对照这官方的文档可以轻易在sdk中找到自己需要调用的api。由于我的需求目前只限于成就、排行榜及用户状态量存储,以下都会围绕这三个模块展开。

首先第一步,作为测试,可以新脚本SteamScript.cs并加入如下代码:public class SteamScript : MonoBehaviour {
void Start() {
if(SteamManager.Initialized) {
string name = SteamFriends.GetPersonaName();
Debug.LogError(name);
}
}
}
注意我们在调用任何Steamworks方法前需要先确认steam是否初始化完成,即SteamManager.Initialized。
SDK主要使用回调方式从服务器异步获取需要的数据避免暂停游戏进程。要使用此SDK的回调,必须在类中先定义protected Callback<>作为一个成员变量注册到回调。如:public class SteamScript : MonoBehaviour {
protected Callback<GameOverlayActivated_t> m_GameOverlayActivated;
}
然后可以利用Callback<>.Create()创建回调。通常把回调创建放在OnEnable方法内以确保Unity加载完成后重复创建回调。如:
public class SteamScript : MonoBehaviour {
private CallResult<NumberOfCurrentPlayers_t> m_NumberOfCurrentPlayers;

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

private void OnNumberOfCurrentPlayers(NumberOfCurrentPlayers_t pCallback, bool bIOFailure) {
if (pCallback.m_bSuccess != 1 || bIOFailure) {
Debug.Log("There was an error retrieving the NumberOfCurrentPlayers.");
}
else {
Debug.Log("The number of players playing your game: " + pCallback.m_cPlayers);
}
}
}

关于回调的结果,声明CallResult<>接收。
public class SteamScript : MonoBehaviour {
private CallResult<NumberOfCurrentPlayers_t> m_NumberOfCurrentPlayers;

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

private void OnNumberOfCurrentPlayers(NumberOfCurrentPlayers_t pCallback, bool bIOFailure) {
if (pCallback.m_bSuccess != 1 || bIOFailure) {
Debug.Log("There was an error retrieving the NumberOfCurrentPlayers.");
}
else {
Debug.Log("The number of players playing your game: " + pCallback.m_cPlayers);
}
}
} 完整的调用方法如下:
public class SteamScript : MonoBehaviour {
private CallResult<NumberOfCurrentPlayers_t> m_NumberOfCurrentPlayers;

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

private void Update() {
if(Input.GetKeyDown(KeyCode.Space)) {
SteamAPICall_t handle = SteamUserStats.GetNumberOfCurrentPlayers();
m_NumberOfCurrentPlayers.Set(handle);
Debug.Log("Called GetNumberOfCurrentPlayers()");
}
}

private void OnNumberOfCurrentPlayers(NumberOfCurrentPlayers_t pCallback, bool bIOFailure) {
if (pCallback.m_bSuccess != 1 || bIOFailure) {
Debug.Log("There was an error retrieving the NumberOfCurrentPlayers.");
}
else {
Debug.Log("The number of players playing your game: " + pCallback.m_cPlayers);
}
}
}

关于SteamManager的工作原理,可参考之前链接文档中的相关模块。
涉及SDK中所有API的用法,前文链接中提供的Demo都有详细的演示,下边总结下自己主要使用的功能模块。

using UnityEngine;
using Steamworks;

public class SteamScript : MonoBehaviour {
protected Callback<GameOverlayActivated_t> m_GameOverlayActivated;
// Use this for initialization
void Start () {
if (SteamManager.Initialized)
{
string name = SteamFriends.GetPersonaName();

}
}

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

private void OnGameOverlayActivated(GameOverlayActivated_t pCallback)
{
if (pCallback.m_bActive != 0)
{
Time.timeScale = 0;
}else
{
Time.timeScale = 1;
}
}
上边这段代码即前边的演示代码,利用OnGameOverlayActivated获取当前steam窗口弹出回调来控制当前游戏主进程的暂停与恢复。

在项目通过青睐之光审核后填写相关公司及税务信息材料可注册成为value的合作伙伴以获取开发权限。之后,即可在项目的后台页面设定项目所需存储的用户信息,以及可自定义成就信息、排行榜信息等。同样这些操作也可以用代码直接完成。下边是完整的测试脚本:

using UnityEngine;
using Steamworks;
using System.Collections.Generic;

public enum EClientGameState
{
k_EClientGameCount,
k_EClientGameWinner,
k_EClientGameLoser,
k_EClientGameAward,
};
// This is a port of StatsAndAchievements.cpp from SpaceWar, the official Steamworks Example.
public class SteamStatsAndAchievements : MonoBehaviour
{
#region 单例
private static SteamStatsAndAchievements ins;

public static SteamStatsAndAchievements Ins
{
get
{
return ins;
}
}
#endregion
void Awake()
{
ins = this;
}

private enum Achievement : int
{
ACH_WIN_ONE_GAME,
ACH_WIN_100_GAMES,
ACH_LEVEL1,
ACH_LEVEL2,
ACH_LEVEL3,
ACH_LEVEL4,
};

private Achievement_t[] m_Achievements = new Achievement_t[] {
new Achievement_t(Achievement.ACH_WIN_ONE_GAME, "初心者", ""),
new Achievement_t(Achievement.ACH_WIN_100_GAMES, "Champion", ""),
new Achievement_t(Achievement.ACH_LEVEL1, "LEVEL1", ""),
new Achievement_t(Achievement.ACH_LEVEL2, "LEVEL2",
4000
""),
new Achievement_t(Achievement.ACH_LEVEL3, "LEVEL3", ""),
new Achievement_t(Achievement.ACH_LEVEL4, "LEVEL4", ""),
};

public List<GameObject> gos;
// Our GameID
private CGameID m_GameID;

// Did we get the stats from Steam?
private bool m_bRequestedStats;
private bool m_bStatsValid;

// Should we store stats this frame?
private bool m_bStoreStats;

private SteamLeaderboard_t m_steamLeaderboard;
// Persisted Stat details
private int m_nTotalGamesPlayed;
private int m_nTotalNumWins;
private int m_nTotalNumLosses;
private int m_flGamePlayerAward;
private int m_flMaxPlayerAward;

protected Callback<UserStatsReceived_t> m_UserStatsReceived;
protected Callback<UserStatsStored_t> m_UserStatsStored;
protected Callback<UserAchievementStored_t> m_UserAchievementStored;

protected CallResult<LeaderboardFindResult_t> m_LeaderboardFindResult;
protected CallResult<LeaderboardScoreUploaded_t> m_LeaderboardScoreUploaded;
protected CallResult<LeaderboardScoresDownloaded_t> m_LeaderboardScoresDownloaded;
void OnEnable()
{
if (!SteamManager.Initialized)
return;

// Cache the GameID for use in the Callbacks
m_GameID = new CGameID(SteamUtils.GetAppID());

m_UserStatsReceived = Callback<UserStatsReceived_t>.Create(OnUserStatsReceived);
m_UserStatsStored = Callback<UserStatsStored_t>.Create(OnUserStatsStored);
m_UserAchievementStored = Callback<UserAchievementStored_t>.Create(OnAchievementStored);

m_LeaderboardFindResult = CallResult<LeaderboardFindResult_t>.Create(OnLeaderboardFindResult);

m_LeaderboardScoreUploaded = CallResult<LeaderboardScoreUploaded_t>.Create(OnLeaerboardScoreUploaded);
m_LeaderboardScoresDownloaded = CallResult<LeaderboardScoresDownloaded_t>.Create(OnLeaderboardScoresDownloaded);
// These need to be reset to get the stats upon an Assembly reload in the Editor.
m_bRequestedStats = false;
m_bStatsValid = false;
SteamAPICall_t handle = SteamUserStats.FindOrCreateLeaderboard("PlayerValue", ELeaderboardSortMethod.k_ELeaderboardSortMethodDescending, ELeaderboardDisplayType.k_ELeaderboardDisplayTypeNumeric);
m_LeaderboardFindResult.Set(handle);

}

private void OnLeaderboardScoresDownloaded(LeaderboardScoresDownloaded_t pCallback, bool bIOFailure)
{
if (pCallback.m_hSteamLeaderboard == m_steamLeaderboard)
{
if (pCallback.m_cEntryCount > 0)
{
Debug.LogError("排行榜数据量:" + pCallback.m_cEntryCount);
for (int i = 0; i < pCallback.m_cEntryCount; i++)
{
LeaderboardEntry_t leaderboardEntry;
int[] details = new int[pCallback.m_cEntryCount];
SteamUserStats.GetDownloadedLeaderboardEntry(pCallback.m_hSteamLeaderboardEntries, i, out leaderboardEntry, details, pCallback.m_cEntryCount);
Debug.LogError("用户ID:" + leaderboardEntry.m_steamIDUser + "用户分数" + leaderboardEntry.m_nScore + "用户排名" + leaderboardEntry.m_nGlobalRank + "Details" + leaderboardEntry.m_cDetails + " " + SteamFriends.GetFriendPersonaName(leaderboardEntry.m_steamIDUser));
gos[i].transform.FindChild("name").GetComponent<UILabel>().text = leaderboardEntry.m_nGlobalRank + " " + SteamFriends.GetFriendPersonaName(leaderboardEntry.m_steamIDUser);
gos[i].transform.FindChild("score").GetComponent<UILabel>().text = leaderboardEntry.m_nScore+"";
}

}
else
{
Debug.LogError("排行榜数据为空!");
}
}
}

private void OnLeaerboardScoreUploaded(LeaderboardScoreUploaded_t pCallback, bool bIOFailure)
{
if (pCallback.m_bSuccess != 1)
{
UploadScore(PlayerControl.Ins.Award);

}
//else if(pCallback.m_hSteamLeaderboard!= m_steamLeaderboard)
//{
// Debug.LogError("获取排行榜数据信息有误!!");
//}
else
{
Debug.LogError("成功上传价值数据:" + pCallback.m_nScore + "榜内数据是否需要变更:" + pCallback.m_bScoreChanged
+ "新的排名:" + pCallback.m_nGlobalRankNew + "上次排名:" + pCallback.m_nGlobalRankPrevious);
gos[gos.Count - 1].transform.FindChild("name").GetComponent<UILabel>().text = pCallback.m_nGlobalRankNew + " " + SteamFriends.GetPersonaName();
gos[gos.Count - 1].transform.FindChild("score").GetComponent<UILabel>().text = pCallback.m_nScore + "";
if (pCallback.m_nGlobalRankPrevious == 0|| pCallback.m_bScoreChanged != 0)
{

}
DownloadLeaderboardEntries();
}
}

private void OnLeaderboardFindResult(LeaderboardFindResult_t pCallback, bool bIOFailure)
{
if (!SteamManager.Initialized)
return;
if (pCallback.m_hSteamLeaderboard.m_SteamLeaderboard == 0||pCallback.m_bLeaderboardFound==0)
{
Debug.LogError("There is no Leaderboard found");
}else
{
m_steamLeaderboard = pCallback.m_hSteamLeaderboard;
UploadScore(PlayerControl.Ins.Award);
}

}

public void UploadScore(int score)
{
if (m_steamLeaderboard.m_SteamLeaderboard != 0)
{
Debug.LogError("上传分数");
var handle = SteamUserStats.UploadLeaderboardScore(m_steamLeaderboard, ELeaderboardUploadScoreMethod.k_ELeaderboardUploadScoreMethodForceUpdate, score, null, 0);
m_LeaderboardScoreUploaded.Set(handle);
}
}

public void DownloadLeaderboardEntries()
{
if (m_steamLeaderboard.m_SteamLeaderboard != 0)
{
Debug.LogError("请求排行榜数据");
var handle = SteamUserStats.DownloadLeaderboardEntries(m_steamLeaderboard, ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobal, 1, 8);
m_LeaderboardScoresDownloaded.Set(handle);
}
}

private void Update()
{
if (!SteamManager.Initialized)
return;

if (!m_bRequestedStats)
{
// Is Steam Loaded? if no, can't get stats, done
if (!SteamManager.Initialized)
{
m_bRequestedStats = true;
return;
}

// If yes, request our stats
bool bSuccess = SteamUserStats.RequestCurrentStats();

// This function should only return false if we weren't logged in, and we already checked that.
// But handle it being false again anyway, just ask again later.
m_bRequestedStats = bSuccess;
}

if (!m_bStatsValid)
return;

// Get info from sources

// Evaluate achievements
foreach (Achievement_t achievement in m_Achievements)
{
if (achievement.m_bAchieved)
continue;
switch (achievement.m_eAchievementID)
{
case Achievement.ACH_WIN_ONE_GAME:
if (m_nTotalNumWins != 0)
{
UnlockAchievement(achievement);
}
break;
case Achievement.ACH_WIN_100_GAMES:
if (m_nTotalNumWins >= 100)
{
UnlockAchievement(achievement);
}
break;
case Achievement.ACH_LEVEL1:
if (m_flMaxPlayerAward >= 25000)
{
UnlockAchievement(achievement);
}
break;
case Achievement.ACH_LEVEL2:
if (m_flMaxPlayerAward >= 90000)
{
UnlockAchievement(achievement);
}
break;
case Achievement.ACH_LEVEL3:
if (m_flMaxPlayerAward >= 200000)
{
UnlockAchievement(achievement);
}
break;
case Achievement.ACH_LEVEL4:
if (m_flMaxPlayerAward >= 400000)
{
UnlockAchievement(achievement);
}
break;
}
}

//Store stats in the Steam database if necessary
if (m_bStoreStats)
{
// already set any achievements in UnlockAchievement

// set stats
SteamUserStats.SetStat("NumGames", m_nTotalGamesPlayed);
SteamUserStats.SetStat("NumWins", m_nTotalNumWins);
SteamUserStats.SetStat("NumLosses", m_nTotalNumLosses);
SteamUserStats.SetStat("GamePlayerAward", m_flGamePlayerAward);
SteamUserStats.SetStat("MaxPlayerAward", m_flMaxPlayerAward);

bool bSuccess = SteamUserStats.StoreStats();
// If this failed, we never sent anything to the server, try
// again later.
m_bStoreStats = !bSuccess;
}
}

//-----------------------------------------------------------------------------
// Purpose: Game state has changed
//-----------------------------------------------------------------------------
public void OnGameStateChange(EClientGameState eNewState)
{
if (!m_bStatsValid)
return;
//游戏场次
if (eNewState == EClientGameState.k_EClientGameCount)
{
m_nTotalGamesPlayed++;
}
//玩家价值(成就)
else if (eNewState == EClientGameState.k_EClientGameAward)
{
m_flGamePlayerAward = PlayerControl.Ins.Award;
if (m_flGamePlayerAward > m_flMaxPlayerAward)
m_flMaxPlayerAward = m_flGamePlayerAward;
}
//玩家获胜场次
else if (eNewState == EClientGameState.k_EClientGameWinner)
{
m_nTotalNumWins++;
}
//玩家失败场次
else
{
m_nTotalNumLosses++;
}
// We want to update stats the next frame.
m_bStoreStats = true;
}

//-----------------------------------------------------------------------------
// Purpose: Unlock this achievement
//-----------------------------------------------------------------------------
private void UnlockAchievement(Achievement_t achievement)
{
achievement.m_bAchieved = true;

// the icon may change once it's unlocked
//achievement.m_iIconImage = 0;

// mark it down
SteamUserStats.SetAchievement(achievement.m_eAchievementID.ToString());

// Store stats end of frame
m_bStoreStats = true;
}

//-----------------------------------------------------------------------------
// Purpose: We have stats data from Steam. It is authoritative, so update
// our data with those results now.
//-----------------------------------------------------------------------------
private void OnUserStatsReceived(UserStatsReceived_t pCallback)
{
if (!SteamManager.Initialized)
return;

// we may get callbacks for other games' stats arriving, ignore them
if ((ulong)m_GameID == pCallback.m_nGameID)
{
if (EResult.k_EResultOK == pCallback.m_eResult)
{
Debug.Log("Received stats and achievements from Steam\n");

m_bStatsValid = true;

// load achievements
foreach (Achievement_t ach in m_Achievements)
{
bool ret = SteamUserStats.GetAchievement(ach.m_eAchievementID.ToString(), out ach.m_bAchieved);
if (ret)
{
ach.m_strName = SteamUserStats.GetAchievementDisplayAttribute(ach.m_eAchievementID.ToString(), "name");
ach.m_strDescription = SteamUserStats.GetAchievementDisplayAttribute(ach.m_eAchievementID.ToString(), "desc");
Debug.LogError(ach.m_eAchievementID.ToString() + " " + ach.m_bAchieved + " " + ach.m_strName + " " + ach.m_strDescription);
}
else
{
Debug.LogWarning("SteamUserStats.GetAchievement failed for Achievement " + ach.m_eAchievementID + "\nIs it registered in the Steam Partner site?");
}
}

// load stats
SteamUserStats.GetStat("NumGames", out m_nTotalGamesPlayed);
SteamUserStats.GetStat("NumWins", out m_nTotalNumWins);
SteamUserStats.GetStat("NumLosses", out m_nTotalNumLosses);
SteamUserStats.GetStat("GamePlayerAward", out m_flGamePlayerAward);
SteamUserStats.GetStat("MaxPlayerAward", out m_flMaxPlayerAward);
}
else
{
Debug.Log("RequestStats - failed, " + pCallback.m_eResult);
}
}
}

//-----------------------------------------------------------------------------
// Purpose: Our stats data was stored!
//-----------------------------------------------------------------------------
private void OnUserStatsStored(UserStatsStored_t pCallback)
{
// we may get callbacks for other games' stats arriving, ignore them
if ((ulong)m_GameID == pCallback.m_nGameID)
{
if (EResult.k_EResultOK == pCallback.m_eResult)
{
Debug.Log("StoreStats - success");
}
else if (EResult.k_EResultInvalidParam == pCallback.m_eResult)
{
// One or more stats we set broke a constraint. They've been reverted,
// and we should re-iterate the values now to keep in sync.
Debug.Log("StoreStats - some failed to validate");
// Fake up a callback here so that we re-load the values.
UserStatsReceived_t callback = new UserStatsReceived_t();
callback.m_eResult = EResult.k_EResultOK;
callback.m_nGameID = (ulong)m_GameID;
OnUserStatsReceived(callback);
}
else
{
Debug.Log("StoreStats - failed, " + pCallback.m_eResult);
}
}
}

//void OnGUI()
//{
// if (GUI.Button(new Rect(10, 10, 50, 50), "reset"))
// {
// SteamUserStats.ResetAllStats(true);
// SteamUserStats.RequestCurrentStats();
// }

//}

//-----------------------------------------------------------------------------
// Purpose: An achievement was stored
//-----------------------------------------------------------------------------
private void OnAchievementStored(UserAchievementStored_t pCallback)
{
// We may get callbacks for other games' stats arriving, ignore them
if ((ulong)m_GameID == pCallback.m_nGameID)
{
if (0 == pCallback.m_nMaxProgress)
{
Debug.Log("Achievement '" + pCallback.m_rgchAchievementName + "' unlocked!");
}
else
{
Debug.Log("Achievement '" + pCallback.m_rgchAchievementName + "' progress callback, (" + pCallback.m_nCurProgress + "," + pCallback.m_nMaxProgress + ")");
}
}
}

private class Achievement_t
{
public Achievement m_eAchievementID;
public string m_strName;
public string m_strDescription;
public bool m_bAchieved;

/// <summary>
/// Creates an Achievement. You must also mirror the data provided here in https://partner.steamgames.com/apps/achievements/yourappid /// </summary>
/// <param name="achievement">The "API Name Progress Stat" used to uniquely identify the achievement.</param>
/// <param
99bd
name="name">The "Display Name" that will be shown to players in game and on the Steam Community.</param>
/// <param name="desc">The "Description" that will be shown to players in game and on the Steam Community.</param>
public Achievement_t(Achievement achievementID, string name, string desc)
{
m_eAchievementID = achievementID;
m_strName = name;
m_strDescription = desc;
m_bAchieved = false;
}
}
}
EClientGameState作为自定义的成就类型划分,以区分预设成就,在玩家游戏状态发生改变时上传更新服务器端数据。提示下没接触过steamworks的小伙伴,value允许搭建自己的服务器做后台,也可以直接使用steam提供的后台作为服务器。后者类似于Unity提供的PlayerPrefs,数据存取十分简单。
以上逻辑总体遵循前边所述的规则,注册回调后调用Callback<>.Create()来绑定数据返回的后续操作。有两点要注意在使用排行榜上传数据时,需要指定排行榜的行为(分为none,保留玩家最佳成绩及总是替换玩家当前数据)。若使用ELeaderboardUploadScoreMethod.k_ELeaderboardUploadScoreMethodKeepBest,需注意第一次上传时不会更新排行榜数据。这里测试后暂时还没找到强制更新排行榜数据的方法,所以采用了ELeaderboardUploadScoreMethod.k_ELeaderboardUploadScoreMethodForceUpdate。

另一个要注意的点就是有的回调采用了handle的形式,每次调用需获取对应的句柄并使用Set方法设置句柄。这里最初没有注意采用数据存储一样的方式注册回调导致无法上传成功取得返回结果。

最后值得注意的一点是向项目后台上传项目元数据时要使用原生sdk中提供的工具,在sdk目录的tools文件夹下。根据steam提供给你的项目的两个id,修改对应的配置文件以后把所有项目元数据置于文件夹下利用命令行上传。这个在原生sdk的文档中有youtube视频教程说明。要注意的是视频中的命令无法全部上传成功,所以我尝试采用了数据库的select *思路加了*强行上传目录下全部文件,成功上传全部数据。

差不多就是这些,第一次写博客,本人技术能力也不是很强,作为总结也希望能给小伙伴们带来一定帮助。

最后感谢github上开源的Steamworks.NET!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息