1. Giriş
Son Güncelleme: 2023-01-27
Skor tablosu oluşturmak için ne gerekir?
Puan tabloları temelde yalnızca puan tablolarıdır. Ancak bir puanın sıralamasını okumak için diğer tüm puanların belirli bir sırayla bilinmesi gerekir. Ayrıca, oyununuz popüler olursa skor tablolarınız büyür ve sık sık okunup yazılır. Başarılı bir skor tablosu oluşturmak için bu sıralama işleminin hızlı bir şekilde gerçekleştirilmesi gerekir.
Ne oluşturacaksınız?
Bu codelab'de, her biri farklı bir senaryoya uygun çeşitli skor tabloları uygulayacaksınız.
Neler öğreneceksiniz?
Aşağıdaki dört farklı skor tablosunu nasıl uygulayacağınızı öğreneceksiniz:
- Sıralamayı belirlemek için basit kayıt sayma yönteminin kullanıldığı temel bir uygulama
- Ucuz ve düzenli olarak güncellenen bir skor tablosu
- Biraz ağaç saçmalığı içeren anlık skor tablosu
- Çok büyük oyuncu kitlelerinin yaklaşık olarak sıralanması için stokastik (olasılıklı) bir skor tablosu
Gerekenler
- Chrome'un son sürümü (107 veya sonraki sürümler)
- Node.js 16 veya daha yeni bir sürüm (nvm kullanıyorsanız sürüm numaranızı görmek için
nvm --version
komutunu çalıştırın) - Ücretli Firebase Blaze planı (isteğe bağlı)
- Firebase CLI v11.16.0 veya üzeri
CLI'yı yüklemek içinnpm install -g firebase-tools
komutunu çalıştırabilir ya da daha fazla yükleme seçeneği için CLI belgelerine göz atabilirsiniz. - JavaScript, Cloud Firestore, Cloud Functions ve Chrome Geliştirici Araçları hakkında bilgi sahibi olma
2. Hazırlanma
Kodu edinin
Bu proje için ihtiyacınız olan her şeyi bir Git deposuna yerleştirdik. Başlamak için kodu alıp en sevdiğiniz geliştirme ortamında açmanız gerekir. Bu codelab'de VS Code kullandık ancak herhangi bir metin düzenleyiciyi de kullanabilirsiniz.
ve indirilen ZIP dosyasını açın.
Alternatif olarak, istediğiniz dizine klonlayın:
git clone https://github.com/FirebaseExtended/firestore-leaderboards-codelab.git
Başlangıç noktamız nedir?
Projemiz şu anda bazı boş işlevlere sahip boş bir sayfa:
index.html
, geliştirici konsolundan işlevleri çağırmamıza ve çıkışlarını görmemize olanak tanıyan bazı yapıştırıcı komut dosyaları içerir. Bunu, arka ucumuzla arayüz oluşturmak ve işlev çağrılarımızın sonuçlarını görmek için kullanacağız. Gerçek hayattaki bir senaryoda, bu arka uç çağrılarını doğrudan oyununuzdan yaparsınız. Her puan eklemek istediğinizde oyun oynamak çok uzun süreceği için bu codelab'de oyun kullanmıyoruz.functions/index.js
, tüm Cloud Functions'larımızı içerir.addScores
vedeleteScores
gibi bazı yardımcı işlevlerin yanı sıra bu codelab'de uygulayacağımız ve başka bir dosyadaki yardımcı işlevleri çağıran işlevleri de görürsünüz.functions/functions-helpers.js
, uygulayacağımız boş işlevleri içerir. Her skor tablosu için okuma, oluşturma ve güncelleme işlevlerini uygulayacağız. Uygulama seçimimizin hem uygulamamızın karmaşıklığını hem de ölçeklendirme performansını nasıl etkilediğini göreceksiniz.functions/utils.js
daha fazla yardımcı işlev içerir. Bu codelab'de bu dosyaya dokunmayacağız.
Firebase projesi oluşturma ve ayarlama
Yeni bir Firebase projesi oluşturma
- Google Hesabınızı kullanarak Firebase konsolunda oturum açın.
- Yeni bir proje oluşturmak için düğmeyi tıklayın ve ardından bir proje adı girin (örneğin,
Leaderboards Codelab
).
- Devam'ı tıklayın.
- İstenirse Firebase şartlarını inceleyip kabul edin ve Devam'ı tıklayın.
- (İsteğe bağlı) Firebase konsolunda yapay zeka yardımını etkinleştirin ("Firebase'de Gemini" olarak adlandırılır).
- Bu codelab için Google Analytics'e ihtiyacınız yoktur. Bu nedenle, Google Analytics seçeneğini devre dışı bırakın.
- Proje oluştur'u tıklayın, projenizin hazırlanmasını bekleyin ve ardından Devam'ı tıklayın.
Firebase ürünlerini ayarlama
- Oluştur menüsünde İşlevler'i tıklayın ve istenirse Blaze fiyatlandırma planını kullanmak için projenizi yükseltin.
- Oluştur menüsünde Firestore veritabanı'nı tıklayın.
- Açılan Veritabanı oluştur iletişim kutusunda Test modunda başlat'ı seçip Sonraki'yi tıklayın.
- Cloud Firestore konumu açılır listesinden bir bölge seçin ve Etkinleştir'i tıklayın.
Liderlik tablonuzu yapılandırma ve çalıştırma
- Terminalde proje köküne gidin ve
firebase use --add
komutunu çalıştırın. Yeni oluşturduğunuz Firebase projesini seçin. - Projenin kök dizininde
firebase emulators:start --only hosting
komutunu çalıştırın. - Tarayıcınızda
localhost:5000
adresine gidin. - Chrome Geliştirici Araçları'nın JavaScript konsolunu açın ve
leaderboard.js
öğesini içe aktarın:const leaderboard = await import("http://localhost:5000/scripts/leaderboard.js");
- Konsolda
leaderboard.codelab();
komutunu çalıştırın. Bir karşılama mesajı görürseniz her şey hazır demektir. Görünmüyorsa emülatörü kapatın ve 2-4 arasındaki adımları yeniden uygulayın.
İlk skor tablosu uygulamasına geçelim.
3. Basit bir skor tablosu uygulama
Bu bölümün sonunda, liderlik tablosuna bir puan ekleyebilecek ve sıralamamızı öğrenebileceğiz.
Başlamadan önce bu skor tablosu uygulamasının nasıl çalıştığını açıklayalım: Tüm oyuncular tek bir koleksiyonda saklanır ve bir oyuncunun sıralaması, koleksiyon alınarak ve oyuncunun önünde kaç oyuncu olduğu sayılarak belirlenir. Bu sayede puan eklemek ve güncellemek kolaylaşır. Yeni bir puan eklemek için puanı koleksiyona ekleriz. Puanı güncellemek için ise mevcut kullanıcımızı filtreler ve sonuçta elde edilen dokümanı güncelleriz. Bunun kodda nasıl göründüğüne bakalım.
functions/functions-helper.js
içinde, olabildiğince basit olan createScore
işlevini uygulayın:
async function createScore(score, playerID, firestore) {
return firestore.collection("scores").doc().create({
user: playerID,
score: score,
});
}
Puanları güncellemek için, güncellenen puanın zaten mevcut olduğundan emin olmak üzere bir hata kontrolü eklememiz yeterlidir:
async function updateScore(playerID, newScore, firestore) {
const playerSnapshot = await firestore.collection("scores")
.where("user", "==", playerID).get();
if (playerSnapshot.size !== 1) {
throw Error(`User not found in leaderboard: ${playerID}`);
}
const player = playerSnapshot.docs[0];
const doc = firestore.doc(player.id);
return doc.update({
score: newScore,
});
}
Son olarak, basit ancak daha az ölçeklenebilir sıralama işlevimiz:
async function readRank(playerID, firestore) {
const scores = await firestore.collection("scores")
.orderBy("score", "desc").get();
const player = `${playerID}`;
let rank = 1;
for (const doc of scores.docs) {
const user = `${doc.get("user")}`;
if (user === player) {
return {
user: player,
rank: rank,
score: doc.get("score"),
};
}
rank++;
}
// No user found
throw Error(`User not found in leaderboard: ${playerID}`);
}
Haydi, bu özelliği deneyelim. Terminalde aşağıdakileri çalıştırarak işlevlerinizi dağıtın:
firebase deploy --only functions
Ardından, Chrome'un JS konsoluna başka puanlar ekleyerek diğer oyuncular arasındaki sıralamamızı görelim.
leaderboard.addScores(); // Results may take some time to appear.
Şimdi kendi puanımızı da ekleyebiliriz:
leaderboard.addScore(999, 11); // You can make up a score (second argument) here.
Yazma işlemi tamamlandığında konsolda "Puan oluşturuldu" yanıtını görürsünüz. Bunun yerine hata mı görüyorsunuz? Neyin yanlış gittiğini görmek için Firebase konsolundan Functions günlüklerini açın.
Son olarak, puanımızı alıp güncelleyebiliriz.
leaderboard.getRank(999);
leaderboard.updateScore(999, 0);
leaderboard.getRank(999); // we should be last place now (11)
Ancak bu uygulama, belirli bir puanın sıralamasını getirmek için istenmeyen doğrusal zaman ve bellek gereksinimlerine neden olur. İşlev yürütme süresi ve bellek sınırlı olduğundan bu durum, yalnızca getirme işlemlerimizin giderek yavaşlayacağı anlamına gelmez. Yeterli puan eklendikten sonra işlevlerimiz, sonuç döndürmeden önce zaman aşımına uğrar veya kilitlenir. Bir avuç oyuncunun ötesine geçmek istiyorsak daha iyi bir şeye ihtiyacımız olduğu açık.
Firestore'u sık kullanıyorsanız bu skor tablosunu çok daha performanslı hale getirecek olan COUNT toplama sorgularını biliyor olabilirsiniz. Haklısınız. SAYMA sorgularında bu, bir milyon kadar kullanıcının altında iyi ölçeklenir ancak performansı yine de doğrusal olur.
Ancak koleksiyondaki tüm dokümanları zaten numaralandıracaksak her dokümana bir sıra atayabiliriz. Böylece, dokümanı getirmemiz gerektiğinde getirme işlemlerimiz O(1) sürede ve bellekte gerçekleşir. Bu durum, bizi bir sonraki yaklaşımımız olan düzenli olarak güncellenen skor tablosuna götürüyor.
4. Periyodik olarak güncellenen bir skor tablosu uygulama
Bu yaklaşımın anahtarı, sıralamayı belgenin kendisinde depolamaktır. Böylece sıralamayı getirmek için ek bir işlem yapmamıza gerek kalmaz. Bunu başarmak için yeni bir tür işleve ihtiyacımız var.
index.js
alanına aşağıdakileri ekleyin:
// Also add this to the top of your file
const admin = require("firebase-admin");
exports.scheduledFunctionCrontab = functions.pubsub.schedule("0 2 * * *")
// Schedule this when most of your users are offline to avoid
// database spikiness.
.timeZone("America/Los_Angeles")
.onRun((context) => {
const scores = admin.firestore().collection("scores");
scores.orderBy("score", "desc").get().then((snapshot) => {
let rank = 1;
const writes = [];
for (const docSnapshot of snapshot.docs) {
const docReference = scores.doc(docSnapshot.id);
writes.push(docReference.set({rank: rank}, admin.firestore.SetOptions.merge()));
rank++;
}
Promise.all(writes).then((result) => {
console.log(`Writes completed with results: ${result}`);
});
});
return null;
});
Artık okuma, güncelleme ve yazma işlemlerimiz çok daha kolay. Yazma ve güncelleme izinleri değişmezken okuma izni (functions-helpers.js
içinde) şu şekilde değişir:
async function readRank(playerID, firestore) {
const scores = firestore.collection("scores");
const playerSnapshot = await scores
.where("user", "==", playerID).get();
if (playerSnapshot.size === 0) {
throw Error(`User not found in leaderboard: ${playerID}`);
}
const player = playerSnapshot.docs[0];
if (player.get("rank") === undefined) {
// This score was added before our scheduled function could run,
// but this shouldn't be treated as an error
return {
user: playerID,
rank: null,
score: player.get("score"),
};
}
return {
user: playerID,
rank: player.get("rank"),
score: player.get("score"),
};
}
Maalesef projenize bir faturalandırma hesabı eklemeden bunu dağıtıp test edemezsiniz. Faturalandırma hesabınız varsa planlanmış işlevdeki aralığı kısaltın ve işlevinizin, skor tablonuzdaki puanlara sihirli bir şekilde sıralama atamasını izleyin.
Değilse planlanmış işlevi silin ve bir sonraki uygulamaya geçin.
Bir sonraki bölüme hazırlanmak için puan koleksiyonunun yanındaki 3 noktayı tıklayarak Firestore veritabanınızdaki puanları silin.
5. Gerçek zamanlı ağaç skor tablosu uygulama
Bu yaklaşım, arama verilerini veritabanı koleksiyonunda depolayarak çalışır. Amacımız, tek tip bir koleksiyon oluşturmak yerine her şeyi, dokümanlar arasında hareket ederek geçebileceğimiz bir ağaçta saklamaktır. Bu sayede, belirli bir puanın sıralaması için ikili (veya n-li) arama yapabiliriz. Bu nasıl görünebilir?
Başlangıç olarak, puanlarımızı kabaca eşit gruplara dağıtabilmemiz gerekir. Bunun için kullanıcılarımızın kaydettiği puanların değerleri hakkında bilgi sahibi olmamız gerekir. Örneğin, rekabetçi bir oyunda beceri puanı için bir skor tablosu oluşturuyorsanız kullanıcılarınızın beceri puanları neredeyse her zaman normal dağılımlı olur. Rastgele puan oluşturma işlevimiz, JavaScript'in Math.random()
işlevini kullanır. Bu işlev, yaklaşık olarak eşit bir dağılım sağlar. Bu nedenle, gruplarımızı eşit şekilde böleceğiz.
Bu örnekte, basitlik için 3 paket kullanacağız. Ancak bu uygulamayı gerçek bir uygulamada kullanırsanız daha fazla paketle daha hızlı sonuçlar elde edeceğinizi göreceksiniz. Daha sığ bir ağaç, ortalama olarak daha az koleksiyon getirme işlemi ve daha az kilit çekişmesi anlamına gelir.
Bir oyuncunun sıralaması, daha yüksek puanlı oyuncu sayısının toplamına kendisi için bir puan eklenerek belirlenir. scores
altındaki her koleksiyonda üç belge saklanır. Her belgenin bir aralığı, her aralıkta bulunan belge sayısı ve üç ilgili alt koleksiyon vardır. Bir sıralamayı okumak için bu ağaçta bir puan arayarak ve daha yüksek puanların toplamını takip ederek ilerleriz. Puanımızı bulduğumuzda doğru toplamı da bulmuş oluruz.
Yazma işlemi çok daha karmaşıktır. Öncelikle, aynı anda birden fazla yazma veya okuma işlemi gerçekleştiğinde veri tutarsızlıklarını önlemek için bir işlemdeki tüm yazma işlemlerini yapmamız gerekir. Yeni belgelerimizi yazmak için ağaçta ilerlerken yukarıda açıkladığımız tüm koşulları da korumamız gerekir. Son olarak, bu yeni yaklaşımın tüm ağaç karmaşıklığı ile orijinal belgelerimizin tümünü saklama ihtiyacını birleştirdiğimiz için depolama maliyetimiz biraz artacak (ancak yine de doğrusal olacaktır).
functions-helpers.js
içinde:
async function createScore(playerID, score, firestore) {
/**
* This function assumes a minimum score of 0 and that value
* is between min and max.
* Returns the expected size of a bucket for a given score
* so that bucket sizes stay constant, to avoid expensive
* re-bucketing.
* @param {number} value The new score.
* @param {number} min The min of the previous range.
* @param {number} max The max of the previous range. Must be greater than
* min.
* @return {Object<string, number>} Returns an object containing the new min
* and max.
*/
function bucket(value, min, max) {
const bucketSize = (max - min) / 3;
const bucketMin = Math.floor(value / bucketSize) * bucketSize;
const bucketMax = bucketMin + bucketSize;
return {min: bucketMin, max: bucketMax};
}
/**
* A function used to store pending writes until all reads within a
* transaction have completed.
*
* @callback PendingWrite
* @param {admin.firestore.Transaction} transaction The transaction
* to be used for writes.
* @return {void}
*/
/**
* Recursively searches for the node to write the score to,
* then writes the score and updates any counters along the way.
* @param {number} id The user associated with the score.
* @param {number} value The new score.
* @param {admin.firestore.CollectionReference} coll The collection this
* value should be written to.
* @param {Object<string, number>} range An object with properties min and
* max defining the range this score should be in. Ranges cannot overlap
* without causing problems. Use the bucket function above to determine a
* root range from constant values to ensure consistency.
* @param {admin.firestore.Transaction} transaction The transaction used to
* ensure consistency during tree updates.
* @param {Array<PendingWrite>} pendingWrites A series of writes that should
* occur once all reads within a transaction have completed.
* @return {void} Write error/success is handled via the transaction object.
*/
async function writeScoreToCollection(
id, value, coll, range, transaction, pendingWrites) {
const snapshot = await transaction.get(coll);
if (snapshot.empty) {
// This is the first score to be inserted into this node.
for (const write of pendingWrites) {
write(transaction);
}
const docRef = coll.doc();
transaction.create(docRef, {exact: {score: value, user: id}});
return;
}
const min = range.min;
const max = range.max;
for (const node of snapshot.docs) {
const data = node.data();
if (data.exact !== undefined) {
// This node held an exact score.
const newRange = bucket(value, min, max);
const tempRange = bucket(data.exact.score, min, max);
if (newRange.min === tempRange.min &&
newRange.max === tempRange.max) {
// The scores belong in the same range, so we need to "demote" both
// to a lower level of the tree and convert this node to a range.
const rangeData = {
range: newRange,
count: 2,
};
for (const write of pendingWrites) {
write(transaction);
}
const docReference = node.ref;
transaction.set(docReference, rangeData);
transaction.create(docReference.collection("scores").doc(), data);
transaction.create(
docReference.collection("scores").doc(),
{exact: {score: value, user: id}},
);
return;
} else {
// The scores are in different ranges. Continue and try to find a
// range that fits this score.
continue;
}
}
if (data.range.min <= value && data.range.max > value) {
// The score belongs to this range that may have subvalues.
// Increment the range's count in pendingWrites, since
// subsequent recursion may incur more reads.
const docReference = node.ref;
const newCount = node.get("count") + 1;
pendingWrites.push((t) => {
t.update(docReference, {count: newCount});
});
const newRange = bucket(value, min, max);
return writeScoreToCollection(
id,
value,
docReference.collection("scores"),
newRange,
transaction,
pendingWrites,
);
}
}
// No appropriate range was found, create an `exact` value.
transaction.create(coll.doc(), {exact: {score: value, user: id}});
}
const scores = firestore.collection("scores");
const players = firestore.collection("players");
return firestore.runTransaction((transaction) => {
return writeScoreToCollection(
playerID, score, scores, {min: 0, max: 1000}, transaction, [],
).then(() => {
transaction.create(players.doc(), {
user: playerID,
score: score,
});
});
});
}
Bu uygulama, tek bir yöntem çağrısı ve yalnızca altı satır kod içeren son uygulamamızdan kesinlikle daha karmaşık. Bu yöntemi uyguladıktan sonra veritabanına birkaç puan ekleyip ortaya çıkan ağacın yapısını inceleyin. JS konsolunuzda:
leaderboard.addScores();
Elde edilen veritabanı yapısı, ağaç yapısı net bir şekilde görünür ve ağacın yaprakları tek tek puanları temsil edecek şekilde aşağıdaki gibi görünmelidir.
scores
- document
range: 0-333.33
count: 2
scores:
- document
exact:
score: 18
user: 1
- document
exact:
score: 22
user: 2
Zor kısmı hallettiğimize göre, puanları daha önce açıklandığı gibi ağaçta ilerleyerek okuyabiliriz.
async function readRank(playerID, firestore) {
const players = await firestore.collection("players")
.where("user", "==", playerID).get();
if (players.empty) {
throw Error(`Player not found in leaderboard: ${playerID}`);
}
if (players.size > 1) {
console.info(`Multiple scores with player ${playerID}, fetching first`);
}
const player = players.docs[0].data();
const score = player.score;
const scores = firestore.collection("scores");
/**
* Recursively finds a player score in a collection.
* @param {string} id The player's ID, since some players may be tied.
* @param {number} value The player's score.
* @param {admin.firestore.CollectionReference} coll The collection to
* search.
* @param {number} currentCount The current count of players ahead of the
* player.
* @return {Promise<number>} The rank of the player (the number of players
* ahead of them plus one).
*/
async function findPlayerScoreInCollection(id, value, coll, currentCount) {
const snapshot = await coll.get();
for (const doc of snapshot.docs) {
if (doc.get("exact") !== undefined) {
// This is an exact score. If it matches the score we're looking
// for, return. Otherwise, check if it should be counted.
const exact = doc.data().exact;
if (exact.score === value) {
if (exact.user === id) {
// Score found.
return currentCount + 1;
} else {
// The player is tied with another. In this case, don't increment
// the count.
continue;
}
} else if (exact.score > value) {
// Increment count
currentCount++;
continue;
} else {
// Do nothing
continue;
}
} else {
// This is a range. If it matches the score we're looking for,
// search the range recursively, otherwise, check if it should be
// counted.
const range = doc.data().range;
const count = doc.get("count");
if (range.min > value) {
// The range is greater than the score, so add it to the rank
// count.
currentCount += count;
continue;
} else if (range.max <= value) {
// do nothing
continue;
} else {
const subcollection = doc.ref.collection("scores");
return findPlayerScoreInCollection(
id,
value,
subcollection,
currentCount,
);
}
}
}
// There was no range containing the score.
throw Error(`Range not found for score: ${value}`);
}
const rank = await findPlayerScoreInCollection(playerID, score, scores, 0);
return {
user: playerID,
rank: rank,
score: score,
};
}
Güncellemeler ek bir alıştırma olarak bırakılır. leaderboard.addScore(id, score)
ve leaderboard.getRank(id)
yöntemleriyle JS konsolunuzda puan eklemeyi ve getirmeyi deneyin. Liderlik tablonuzun Firebase konsolunda nasıl değiştiğini görün.
Ancak bu uygulamada, logaritmik performans elde etmek için eklediğimiz karmaşıklığın bir maliyeti vardır.
- İlk olarak, işlemlerin tutarlı kalmasını sağlamak için belgelere okuma ve yazma işlemlerinin kilitlenmesi gerektiğinden bu skor tablosu uygulaması kilit çekişmesi sorunlarıyla karşılaşabilir.
- İkincisi, Firestore 100 alt koleksiyon derinliği sınırı uygular. Bu nedenle, 100 bağlı puandan sonra alt ağaç oluşturmaktan kaçınmanız gerekir. Bu uygulama bunu yapmaz.
- Son olarak, bu skor tablosu yalnızca ağacın dengeli olduğu ideal durumda logaritmik olarak ölçeklenir. Dengesizse bu skor tablosunun en kötü durum performansı yine doğrusal olur.
İşlemi tamamladığınızda Firebase konsolu üzerinden scores
ve players
koleksiyonlarını silin. Ardından, son skor tablosu uygulamamıza geçeceğiz.
6. Stokastik (olasılıklı) skor tablosu uygulama
Ekleme kodunu çalıştırırken, çok fazla kez paralel olarak çalıştırırsanız işlevlerinizin işlem kilidi çekişmesiyle ilgili bir hata mesajıyla başarısız olmaya başladığını fark edebilirsiniz. Bu sorunu çözmenin yolları vardır ancak bu kod laboratuvarında bunları ele almayacağız. Tam sıralamaya ihtiyacınız yoksa hem daha basit hem de daha hızlı bir çözüm için önceki yaklaşımın tüm karmaşıklığını ortadan kaldırabilirsiniz. Oyuncularımızın puanları için tam sıralama yerine tahmini bir sıralama döndürme ve bunun veritabanı mantığımızı nasıl değiştirebileceğine bakalım.
Bu yaklaşımda, skor tablomuzu 100 gruba ayıracağız. Bu grupların her biri, almayı beklediğimiz puanların yaklaşık yüzde birini temsil edecek. Bu yaklaşım, puan dağılımımız hakkında bilgi sahibi olmasak bile işe yarar. Bu durumda, puanların grupta kabaca eşit şekilde dağıtılacağını garanti edemeyiz ancak puanlarımızın nasıl dağıtılacağını bilirsek tahminlerimizde daha fazla kesinlik elde ederiz.
Yaklaşımımız şu şekildedir: Her paket, daha önce olduğu gibi, içindeki puanların sayısını ve puan aralığını saklar. Yeni bir puan eklerken puanın bulunduğu grubu bulup sayısını artırırız. Bir sıralama getirirken, daha fazla arama yapmak yerine yalnızca kendisinden önceki grupları toplar ve ardından grubumuzda yaklaşık bir değer belirleriz. Bu sayede, çok kısa sürede arama ve ekleme yapabiliriz ve çok daha az kod yazmamız gerekir.
Öncelikle ekleme:
// Add this line to the top of your file.
const admin = require("firebase-admin");
// Implement this method (again).
async function createScore(playerID, score, firestore) {
const scores = await firestore.collection("scores").get();
if (scores.empty) {
// Create the buckets since they don't exist yet.
// In a real app, don't do this in your write function. Do it once
// manually and then keep the buckets in your database forever.
for (let i = 0; i < 10; i++) {
const min = i * 100;
const max = (i + 1) * 100;
const data = {
range: {
min: min,
max: max,
},
count: 0,
};
await firestore.collection("scores").doc().create(data);
}
throw Error("Database not initialized");
}
const buckets = await firestore.collection("scores")
.where("range.min", "<=", score).get();
for (const bucket of buckets.docs) {
const range = bucket.get("range");
if (score < range.max) {
const writeBatch = firestore.batch();
const playerDoc = firestore.collection("players").doc();
writeBatch.create(playerDoc, {
user: playerID,
score: score,
});
writeBatch.update(
bucket.ref,
{count: admin.firestore.FieldValue.increment(1)},
);
const scoreDoc = bucket.ref.collection("scores").doc();
writeBatch.create(scoreDoc, {
user: playerID,
score: score,
});
return writeBatch.commit();
}
}
}
Bu ekleme kodunun, veritabanı durumunuzu üstte başlatmak için bazı mantıklar içerdiğini ve üretimde bu tür bir işlem yapmamanız gerektiği konusunda uyarı verdiğini fark edeceksiniz. Başlatma kodu, yarış koşullarına karşı hiç korunmaz. Bu nedenle, bunu yaparsanız eşzamanlı olarak yapılan birden fazla yazma işlemi, size bir sürü yinelenen paket vererek veritabanınızı bozar.
İşlevlerinizi dağıtın ve tüm paketleri sıfır sayısıyla başlatmak için bir ekleme işlemi çalıştırın. Bu işlem, güvenle yok sayabileceğiniz bir hata döndürür.
leaderboard.addScore(999, 0); // The params aren't important here.
Veritabanı doğru şekilde başlatıldığına göre addScores
komutunu çalıştırabilir ve Firebase konsolunda verilerimizin yapısını görebiliriz. Ortaya çıkan yapı, yüzeysel olarak benzer olsa da son uygulamamıza kıyasla çok daha düzdür.
leaderboard.addScores();
Şimdi puanları okumak için:
async function readRank(playerID, firestore) {
const players = await firestore.collection("players")
.where("user", "==", playerID).get();
if (players.empty) {
throw Error(`Player not found in leaderboard: ${playerID}`);
}
if (players.size > 1) {
console.info(`Multiple scores with player ${playerID}, fetching first`);
}
const player = players.docs[0].data();
const score = player.score;
const scores = await firestore.collection("scores").get();
let currentCount = 1; // Player is rank 1 if there's 0 better players.
let interp = -1;
for (const bucket of scores.docs) {
const range = bucket.get("range");
const count = bucket.get("count");
if (score < range.min) {
currentCount += count;
} else if (score >= range.max) {
// do nothing
} else {
// interpolate where the user is in this bucket based on their score.
const relativePosition = (score - range.min) / (range.max - range.min);
interp = Math.round(count - (count * relativePosition));
}
}
if (interp === -1) {
// Didn't find a correct bucket
throw Error(`Score out of bounds: ${score}`);
}
return {
user: playerID,
rank: currentCount + interp,
score: score,
};
}
addScores
işlevinin puanları eşit şekilde dağıtmasını sağladığımız ve gruplar içinde doğrusal enterpolasyon kullandığımız için çok doğru sonuçlar elde edeceğiz, kullanıcı sayısını artırdıkça skor tablomuzun performansı düşmeyecek ve sayıları güncellerken kilitlenmeyle ilgili (çok fazla) endişelenmemize gerek kalmayacak.
7. Ek: Hile
Bir tarayıcı sekmesinin JS konsolu aracılığıyla codelab'ime değerler yazıyorsam oyuncularımın herhangi biri skor tablosuna yalan söyleyip adil bir şekilde elde etmediği yüksek bir skor aldığını söyleyemez mi?
Evet, eşleşebilir. Hile yapılmasını önlemek istiyorsanız bunu yapmanın en sağlam yolu, güvenlik kuralları aracılığıyla istemcilerin veritabanınıza yazmasını devre dışı bırakmak, istemcilerin doğrudan çağıramaması için Cloud Functions işlevlerinize güvenli erişim sağlamak ve ardından skor güncellemelerini skor tablosuna göndermeden önce oyun içi işlemleri sunucunuzda doğrulamaktır.
Bu stratejinin hileye karşı kesin bir çözüm olmadığını belirtmek önemlidir. Hileciler, yeterli teşvikle sunucu tarafı doğrulamalarını atlatmanın yollarını bulabilir. Başarılı birçok büyük video oyunu, yeni hileleri tespit etmek ve yayılmalarını önlemek için hilecilerle sürekli olarak bir kedi fare oyunu oynar. Bu olgunun zorlu bir sonucu, her oyun için sunucu tarafı doğrulamanın doğası gereği özel olmasıdır. Firebase, kullanıcının basit bir komut dosyası oluşturulmuş istemci aracılığıyla oyununuzu kopyalamasını engelleyecek Uygulama Kontrolü gibi kötüye kullanımı önleme araçları sunsa da Firebase, bütünsel bir hile karşıtı hizmet sağlamaz.
Sunucu tarafı doğrulamasının eksik olduğu durumlarda, yeterince popüler bir oyun veya hile yapmanın yeterince kolay olduğu durumlarda, skor tablosunda en üstteki değerlerin tamamı hilecilerden oluşur.
8. Tebrikler
Tebrikler, Firebase'de dört farklı skor tablosu oluşturdunuz. Oyununuzun doğruluk ve hız ihtiyaçlarına bağlı olarak, size uygun olanı makul bir maliyetle seçebilirsiniz.
Ardından, oyunlarla ilgili öğrenme rotalarına göz atın.