从技术上讲,您的问题是如何使用安全规则完成此操作,但由于它有点XY 问题,并且没有排除其他可能性,我也会在这里解决其中的一些问题。
我会做很多假设,因为回答这个问题实际上需要一组完全指定的规则,这些规则需要遵循并且实际上是实现整个应用程序的问题(增加分数是游戏逻辑规则的结果,而不是一个简单的数学问题)。
总分在客户端
也许对这个难题最简单的答案就是没有总分。只需获取玩家列表并手动总计。
当这可能有用时:
- 玩家名单是数百或更少
- 玩家数据适当小(不是每个 500k)
怎么做:
var ref = new Firebase(URL);
function getTotalScore(gameId, callback) {
ref.child('app/games/' + gameId + '/players').once('value', function(playerListSnap) {
var total = 0;
playerListSnap.forEach(function(playerSnap) {
var data = playerSnap.val();
total += data.totalScore || 0;
});
callback(gameId, total);
});
}
使用特权工作者更新分数
一个非常复杂且简单的方法(因为它只需要将安全规则设置为类似的东西".write": "auth.uid === 'SERVER_PROCESS'"
)将使用一个服务器进程来简单地监控游戏并累积总数。这可能是最简单且最容易维护的解决方案,但它的缺点是需要另一个工作部件。
当这可能有用时:
- 您可以启动 Heroku 服务或将 .js 文件部署到 webscript.io
- 5-30 美元范围内的额外每月订阅不会破坏交易
怎么做:
显然,这涉及大量的应用程序设计,并且需要在不同的层次上完成。让我们只关注结束游戏和计算排行榜,因为这是一个很好的例子。
首先将评分代码拆分为自己的路径,例如
/scores_entries/$gameid/$scoreid = < player: ..., score: ... >
/game_scores/$gameid/$playerid = <integer>
现在监视游戏以查看它们何时关闭:
var rootRef = new Firebase(URL);
var gamesRef = rootRef.child('app/games');
var lbRef = rootRef.child('leaderboards');
gamesRef.on('child_added', watchGame);
gamesRef.child('app/games').on('child_remove', unwatchGame);
function watchGame(snap) {
snap.ref().child('status').on('value', gameStatusChanged);
}
function unwatchGame(snap) {
snap.ref().child('status').off('value', gameStatusChanged);
}
function gameStatusChanged(snap) {
if( snap.val() === 'CLOSED' ) {
unwatchGame(snap);
calculateScores(snap.name());
}
}
function calculateScores(gameId) {
gamesRef.child(gameId).child('users').once('value', function(snap) {
var userScores = {};
snap.forEach(function(ss) {
var score = ss.val() || 0;
userScores[ss.name()] = score;
});
updateLeaderboards(userScores);
});
}
function updateLeaderboards(userScores) {
for(var userId in userScores) {
var score = userScores[userId];
lbRef.child(userId).transaction(function(currentValue) {
return (currentValue||0) + score;
});
}
}
使用审计路径和安全规则
当然,这将是可用选择中最复杂和最困难的。
当这可能有用时:
- 当我们拒绝使用涉及服务器进程的任何其他策略时
- 当非常担心玩家作弊时
- 当我们有很多额外的时间可以燃烧时
显然,我对这种方法有偏见。主要是因为它很难做到正确,并且需要大量的能量,而这些能量可以用少量的货币投资来代替。
要做到这一点,需要对每个单独的写入请求进行审查。有几个明显的要点需要保护(可能更多):
- 编写任何包含分数增量的游戏事件
- 为每个用户编写游戏总数
- 将游戏总数写入排行榜
- 写入每条审计记录
- 确保不会为了提高分数而即时创建和修改多余的游戏
以下是保护这些要点的一些基本原理:
- 使用用户只能添加(不能更新或删除)条目的审计跟踪
- 验证每个审计条目的优先级是否等于当前时间戳
- 根据当前游戏状态验证每个审计条目是否包含有效数据
- 在尝试增加运行总计时利用审计条目
让我们以安全更新排行榜为例。我们将假设以下内容:
- 用户在游戏中的分数有效
- 用户创建了一个审计条目,例如,leaderboard_audit/$userid/$gameid,当前时间戳作为优先级,分数作为值
- 每个用户记录都提前存在于排行榜中
- 只有用户可以更新自己的分数
所以这是我们假设的数据结构:
/games/$gameid/users/$userid/score
/leaderboard_audit/$userid/$gameid/score
/leaderboard/$userid = { last_game: $gameid, score: <int> }
下面是我们的逻辑是如何工作的:
- 游戏分数设置为
/games/$gameid/users/$userid/score
- 审核记录创建于
/leaderboard_audit/$userid/games_played/$gameid
- at 的值
/leaderboard_audit/$userid/last_game
已更新以匹配$gameid
- 排行榜的更新量与
last_game
的审计记录完全相同
这是实际的规则:
{
"rules": {
"leaderboard_audit": {
"$userid": {
"$gameid": {
// newData.exists() ensures records cannot be deleted
".write": "auth.uid === $userid && newData.exists()",
".validate": "
// can only create new records
!data.exists()
// references a valid game
&& root.child('games/' + $gameid).exists()
// has the correct score as the value
&& newData.val() === root.child('games/' + $gameid + '/users/' + auth.uid + '/score').val()
// has a priority equal to the current timestamp
&& newData.getPriority() === now
// is created after the previous last_game or there isn't a last_game
(
!root.child('leaderboard/' + auth.uid + '/last_game').exists() ||
newData.getPriority() > data.parent().child(root.child('leaderboard/' + auth.uid + '/last_game').val()).getPriority()
)
"
}
}
},
"leaderboard": {
"$userid": {
".write": "auth.uid === $userid && newData.exists()",
".validate": "newData.hasChildren(['last_game', 'score'])",
"last_game": {
".validate": "
// must match the last_game entry
newData.val() === root.child('leaderboard_audit/' + auth.uid + '/last_game').val()
// must not be a duplicate
newData.val() !== data.val()
// must be a game created after the current last_game timestamp
(
!data.exists() ||
root.child('leaderboard_audit/' + auth.uid + '/' + data.val()).getPriority()
< root.child('leaderboard_audit/' + auth.uid + '/' + newData.val()).getPriority()
)
"
},
"score": {
".validate": "
// new score is equal to the old score plus the last_game's score
newData.val() === data.val() +
root.child('games/' + newData.parent().child('last_game').val() + '/users/' + auth.uid + '/score').val()
"
}
}
}
}
}