ARTICLES

自作ゲームに手軽にオンラインランキングを導入しよう!

By wraikny

世界中の人とゲームスコアを競い合おう!

概要

拙作の Simple Rankings Server をつかって、ゲームに手軽にオンラインスコアランキング機能を実装する記事です。

Amusement CreatorsのVPSサーバーで稼働しているので、内部の人は設定ファイルを追記してプログラム再起動するだけで使えます!!

また、C#とF#でのクライアント側の サンプルコード を用意してあり、ライブラリ相当の1ファイルをコピペするだけで簡単に使えます。

サーバー側の設定 (3ステップ)

サーバーに実行ファイルを置く (1/3)

(AmuCre鯖を使うならデプロイ済みなのでスキップ)

  1. Simple Rankings Serverをcloneする。

  2. Build (シングルバイナリを生成)

    dotnet tool restore
    dotnet paket restore
    dotnet fake build -t Publish
    
  3. sshなどしてpublish/linux-x64/SimpleRankingsServerconfig.jsonをサーバーに置く。

  4. config.jsonport,directory等を設定してファイルを実行する。

configに設定を記述 (2/3)

サーバーにsshなどで入りましょう。 ファイル編集のおすすめはCyberDuck

(AmuCre鯖を使うなら:ssh周りは鯖管等に手伝ってもらおう!)

以下のようなコードがあります。

config.json

{
    "directory": "databases",

    "port": 12345,

    "games": {
        "SamplGame": {
            "username": "sample",
            "password": "sample",
            "tables": {
                "SampleTable": {
                    "Score1": "Int",
                    "Score2": "Float",
                    "Name": "Text"
                },
                "SampleTable2": {
                    "Score1": "Text",
                    "Name": "Text"
                }
            }
        }
    }
}
  • games: ここにゲームごとの設定を追記します。

    なお、この名前はDBファイル名とAPIエンドポイント名に使われます。 できる限り英数などを使ってください(日本語とかはダメ!)

    また、他のゲームと名前が被らないようにしてください。

  • usernamepassword: Basic認証に用います。

  • tables: ここに各ゲームのテーブル構造を定義します。

    テーブルの名前が被らないようにしてください。

    テーブルの型に指定できるのはInt,Float,Textとなっています。

なお、一度DBが作成された後はDB定義を書き換えないでください。


type TableType = Int | Float | Text

type TableConfig = Map<string, TableType>

type GameConfig = {
  username : string
  password : string
  tables: Map<string, TableConfig>
}

type Config = {
  port : uint16
  directory : string
  games : Map<string, GameConfig>
}

では、今回はこんな風にコードを追加します。

{
    "games": {
        "SampleGame": {
            "$comment": "省略"
        },
        "wraiknysTutorialSTG": {
            "username": "hoge",
            "password": "fuga",
            "tables": {
                "ScoreTable": {
                    "Score": "Int",
                    "Time": "Float",
                    "Name": "Text"
                }
            }
        }
    }
}

SimpleRankingsServerを(再)起動する (3/3)

configを書き換えたら、プログラムを再起動します。 tmuxを利用する前提で話を進めます。

再起動する場合

(AmuCre鯖を使うならこっち)

# ssh接続済み

$ tmux a -t SimpleRankingsServer # tmuxセッションを再開
# -----------------------------------------
# Ctrl + C でプログラムを停止
$ ./SimpleRankingsServer # プログラムを起動
# Ctrl + B その後 D と押してセッションをデタッチ
# -----------------------------------------

新規セッションで起動する場合

こんな感じにファイルを配置しておく

SimpleRankingsServer
- SimpleRankingsServer
- config.json

なお、config.jsonで指定したポートは解放しておいてください。

# ssh接続済み

$ tmux new -s SimpleRankingsServer # 新規tmuxセッションを開始
# -----------------------------------------
$ cd SimpleRankingsServer
$ ./SimpleRankingsServer
# Ctrl + B その後 D と押してセッションをデタッチ
# -----------------------------------------

クライアントの記述 (3ステップ)

サンプルコードをコピペする (1/3)

以下のファイルをコピペしてプロジェクトに含めると、簡単に利用可能です。

他の言語のsampleはPRをお待ちしています。

では、上記のコードを利用してC#でのランキングにアクセスするコードを記述します。

データ定義 (2/3)

先程config.jsonに記述した内容に合わせてデータを定義します。

using System.Runtime.Serialization;

class Program
{
    [DataContract]
    class ScoreData
    {
        [DataMember]
        public int Score { get; set; }

        [DataMember]
        public float Time { get; set; }

        [DataMember]
        public string Name { get; set; }
    }

    const string Url = @"http://example.com:12345/api/wraiknysTutorialSTG";
    const string Username = "hoge";
    const string Password = "fuga";
}

"http://{address}:{port}/api/{GameName}"がURLになります。

DataContract周りはこちらの記事でも見るとよいのではないでしょうか。

C# JSON Serialize - Qiita

サーバーにアクセスする。(3/3)


C#では以下のメソッドを利用可能です。 型だけ抽出したので雰囲気で感じ取ってください。

F#でも大体同じコードを書いてあります。

public class SimpleRankingsServer.Data<T>
{
    public long Id { get; }
    public Guid UserId { get; }
    public DateTime UTCDate { get; }
    public T values { get; }
}

public class SimpleRankingsServer.Client
{
    // データのIdが返ってくる
    // userId: ユーザーを識別するIdを指定。
    public async Task<long> InsertAsync<T>(string tableName, Guid userId, T data);

    public async Task<Data<T>[]> SelectAsync<T>(
        string tableName,
        // 指定したキーで並び替えたデータが返ってくる
        string orderBy = null,
        // trueだと降順のデータが返ってくる
        bool isDescending = true,
        // 返ってくるデータ数を制限する
        int limit = 100
    );
}

それではやっていきます。

using System;
using System.Threading.Tasks;

class Program
{
    static SimpleRankingsServer.Client client =
        new SimpleRankingsServer.Client(Url, Username, Password);

    static async Task Main(string[] args)
    {
        // ユーザー識別用。ファイルに保存などして使い回す。(ToStringとParseで相互変換できる)
        var userId = Guid.NewGuid();

        var scoreData = new ScoreData { Score = 80, Time = 81.6, Name = "nyamnyam" };

        // ランキングに追加する
        var _recordId = await client.InsertAsync("ScoreTable", userId, scoreData);

        // ランキングからデータを取得する
        var data = await client.SelectAsync<ScoreData>("ScoreTable", orderBy: "Score", limit: 10);

        foreach (var x in data)
        {
            Console.WriteLine(x);
        }
    }
}

なお、サーバー側の仕様として

  • 新しいデータを追加する
  • データを取得する

以外のこと(例:既存のデータを書き換える)はできないようにしています。

まとめ


using System;
using System.Threading.Tasks;
using System.Runtime.Serialization;

class Program
{
    [DataContract]
    class ScoreData
    {
        [DataMember]
        public int Score { get; set; }

        [DataMember]
        public float Time { get; set; }

        [DataMember]
        public string Name { get; set; }
    }

    const string Url = @"http://example.com:12345/api/wraiknysTutorialSTG";
    const string Username = "hoge";
    const string Password = "fuga";

    static SimpleRankingsServer.Client client =
        new SimpleRankingsServer.Client(Url, Username, Password);

    static async Task Main(string[] args)
    {
        // ユーザー識別用。ファイルに保存などして使い回す。(ToStringとParseで相互変換できる)
        var userId = Guid.NewGuid();

        var scoreData = new ScoreData { Score = 80, Time = 81.6, Name = "nyamnyam" };

        // ランキングに追加する
        var _recordId = await client.InsertAsync("ScoreTable", userId, scoreData);

        // ランキングからデータを取得する
        var data = await client.SelectAsync<ScoreData>("ScoreTable", orderBy: "Score", limit: 10);

        foreach (var x in data)
        {
            Console.WriteLine(x);
        }
    }
}


type ScoreData = {
  Score : int
  Time : float
  Name : string
}

let [<Literal>] Url = @"http://example.com:12345/api/wraiknysTutorialSTG"
let [<Literal>] Username = "hoge"
let [<Literal>] Password = "fuga"

let client = new SimpleRankingsServer.Client(Url, Username, Password)
let userId = System.Guid.NewGuid()

[<EntryPoint>]
let main _ =
  async {
    let scoreData = { Score = 90; Time = 111.1; Name = "zawaka" }
    let! _recordId = client.AsyncInsert("ScoreTable", userId, scoreData)
    return! client.AsyncSelect<Sample1>("ScoreTable", orderBy = "Score", limit = 5)
  }
  |> Async.Catch
  |> Async.RunSynchronously
  |> printfn "%A"
  0

あとはこれをいい感じのUIに落とし込むだけ! ですね。

なお、セキュリティ上の観点から、重要なデータは置かないようにしてください。 なにかあっても責任は取れません。

おわり

質問等があればTwitter@wraiknyかslackで聞いてください!

それでは。

wraikny/simple-rankings-server - GitHub

SHARE THIS POST