この記事は Akatsuki Advent Calendar 2016 の4日目です。
追記: 中編(?)が投稿されました!
まだ都度通信で消耗してるの? 〜ソシャゲの通信頻度削減の実験〜 (中編) - Qiita
追記: 後編が投稿されました!
まだ都度通信で消耗してるの? 〜ソシャゲの通信頻度削減の実験〜 (後編) - Qiita
はじめに
株式会社アカツキでサーバーサイドエンジニアをやっております。 @sachaos です。
ちなみに僕もまだ消耗しています。
どういう話?
これが都度通信で消耗している状態
これを
理想的な状態
こうする試みの実験
背景
弊社ではソーシャルゲームの開発しています。
ソーシャルゲーム開発においてサーバーとクライアントとの通信はどのタイミング、どのくらいの頻度でやるのか、というのは悩みの種だと思います。
基本的にはユーザーがどのアイテムをゲットしたということや、ユーザーがレベルアップした等のイベントが起こる箇所では、クライアントがユーザーの「行動」をサーバーに送信して、サーバーは「行動」による「結果」を計算し、その「結果」をクライアントに送信、表示する形になります。
つまりゲームでの流れはざっくりと以下のような流れになります。
1. 行動する(マップを移動する等)
2. クライアントが行動をサーバーに送信する
3. サーバーが「結果」を計算し、クライアントに送信する(移動した結果、経験値などが上がりレベルアップした等)
4. クライアントが「結果」を表示する
(1 - 4) を繰り返す。
問題
ゲーム中に「行動」して「結果」を表示する必要があり、それが短いスパンで何度も起こるようなゲームだと、ゲーム中に都度通信しなければなりません。
この場合、以下のような問題が発生します。
サーバーの負荷要因となる。
大規模なゲームのサーバーはボトルネックになりがちです。
可能な限り通信量を削減したい。
通信のオーバーヘッドがかかるので、ゲームのUXが悪くなる。
通信環境が悪いと快適にゲームができません。
みなさんも電車でゲームをプレイしている時にトンネルに入って通信環境が悪くなり、イライラした経験があるのではないでしょうか?
解決策
上記問題は、サーバー側のみが「行動」->「結果」のゲームロジックを持っていたことが原因でした。
これを解決するためにはクライアント側にもロジックをもたせる必要があります。
クライアントで「行動」->「結果」の計算ができればサーバーへ送信せずとも、「結果」の表示をすることができます。
そして最後にまとめてサーバーへ情報を送信してあげれば良さそうです。
クライアントのみにゲームロジックをもたせる場合
クライアント側のみにゲームロジックをもたせた場合、
ゲームのロジックを持たせることでサーバーに送信せずとも、結果を計算し、表示することができようになります。
サーバーにはゲームロジックを持たせていないので、クライアントは「結果」をサーバーに送信することになります。
しかしこのままではチートし放題です。ゲームのロジックが難しい場合、サーバー側でクライアントから送信された「結果」の妥当性を検証することも難しくなってきます。
誰もチートをしないような優しい世界ではないので、基本的にクライアントからの情報を信用してはいけません。
つまりサーバー側にもゲームロジックをもたせる必要が有ります。
サーバー・クライアントにゲームロジックをもたせる場合
サーバー側・クライアント側の双方にゲームロジックを持たせると、クライアント側ではゲームロジックを使って「行動」->「結果」の計算をすることができ、サーバー側はクライアントからゲーム中に起こった「行動」をまとめて受け取って、クライアントでも表示されていた「結果」をサーバー側で再計算し、「結果」をデータベース等に保存することができます。
しかし、ソシャゲ業界ではサーバー側とクライアント側で、同じプログラミング言語を使用して開発することはまれかと思われます。
弊社で開発しているソーシャルゲームは基本的にサーバーサイドはRuby on Rails、クライアントサイドはC#, Unityを使用して開発していますが、ゲームのロジックをサーバー・クライアント間で共通化する場合、同じゲームのロジックを別のプログラミング言語で実装することになってしまいます。
こうしてしまうと、ゲームロジックを変更した時に他方が追従する必要がありますし、ゲームのロジックは複雑な場合もあります、万が一片方の実装にバグが存在した場合、事故が起きてしまう可能性は十分にあります。なるべくこれらは避けたいです。
結論
なので、一つのプログラミング言語でゲームロジックを書き、サーバー・クライアント各々で使用している言語からそれを呼び出すようにすれば良いのではないかと考えました。
今回、その実現可能性を測ってみるために実験を行います。
ゲームロジックを記述するための言語としては、軽量で高速であり、一般的なゲーム開発でも用いられているLuaを使用してみました。
想定しているサーバーサイドはRuby on Rails, クライアントサイドはC# Unityです。
サンプルゲームの説明
- 行動: 3x3マスのマップをプレイヤーが上下左右に1マスづつ移動する。
- 結果: 特定のマスにプレイヤーが移動することでプレイヤーのポイントが増える。
Luaスクリプト
command_functions = {
up = function (current_position)
next_position = {}
next_position["x"] = current_position["x"] + 0
next_position["y"] = current_position["y"] + 1
return next_position
end,
down = function (current_position)
next_position = {}
next_position["x"] = current_position["x"] + 0
next_position["y"] = current_position["y"] - 1
return next_position
end,
right = function (current_position)
next_position = {}
next_position["x"] = current_position["x"] + 1
next_position["y"] = current_position["y"] + 0
return next_position
end,
left = function (current_position)
next_position = {}
next_position["x"] = current_position["x"] - 1
next_position["y"] = current_position["y"] + 0
return next_position
end
}
function move_user(map, current_position, command, point)
current_position = command_functions[command](current_position)
if get_map_point(map, current_position["x"], current_position["y"], 0) == 1 then
point = point + 1
end
return current_position, point
end
function get_map_point(map, x, y, default_point)
map_x_line = map[y]
if map_x_line == nil then
return default_point
else
point = map_x_line[x] or default_point
return point
end
end
-- Argument examples
-- map = {
-- {0, 0, 0},
-- {1, 1, 1},
-- {1, 0, 1}
-- }
-- commands = {"up", "right", "left", "down", "right", "left", "up", "right", "left"}
-- current_position = {x = 1, y = 1}
function game (map, current_position, commands)
point = 0
for _, command in ipairs(commands) do
current_position, point = move_user(map, current_position, command, point)
end
return current_position, point
end
サーバーの実装
RubyからLuaスクリプトを実行するために rufus-lua という gemを使用しました。
jmettraux/rufus-lua: embedding Lua in Ruby, via FFI
レポジトリ
sachaos/sample-game: sample game.
軽く説明
基本的にはRubyから上記Luaスクリプトを呼び出すだけです。
コントローラーにゴリゴリ書いてもいいのですが、少し使いやすいようにラッパーを作成しました。
Luaスクリプトのフォルダ構成
$ tree ./lib/
./lib/
├── lua_base.rb
└── move_user.rb
ラッパー
ラッパーの抽象クラスです。
これを継承することで、クラス名に対応するLuaスクリプトをlibフォルダから探してロードするようにしています。
move_user.lua のスクリプトに定義されている、 gameメソッドを呼ぶ場合は MoveUser.game(arg)
のように呼び出すことができるようになります。
この仕組みは method_missing
を使用しています。Ruby最高ですね。
Luaのグローバル変数の一覧は _G
で取得できるので、そこから対応するメソッドを探索し、呼び出します。
class LuaBase
attr_reader :state, :global_variables
delegate :eval, to: :state
def initialize
# 抽象クラスだよ、継承して使ってねという主張
raise "Abstract Class" if self.class == LuaBase
# rufus-luaのstateを作成、luaスクリプトを読み込む
@state = Rufus::Lua::State.new
@lua_script = File.read("lib/#{self.class.name.underscore}.lua")
@state.eval(@lua_script)
# グローバル変数を格納しておく
@global_variables = @state.eval("return _G").to_ruby.symbolize_keys
end
def method_missing(method_name, *args)
rufus_object = @global_variables[method_name]
if rufus_object.is_a?(Rufus::Lua::Function)
return rufus_object.call(*args)
end
super
end
end
# lib以下に存在する .luaファイルの名前から動的にクラス生成
# move_user.luaが存在すれば MoveUser クラスが生成される
Dir.glob("lib/*.lua").each do |file_path|
filename = File.basename(file_path).ext
self.class.const_set(filename.classify.to_s, Class.new(LuaBase))
end
行動を送信するAPI PUT /user
UsersController#updateにて、上記クラスを呼び出します。簡単ですね。
class UsersController < ApplicationController
...
def update
# Luaスクリプト move_user.lua を呼び出す
move_user = MoveUser.new
# move_user.lua で定義されているgameメソッドを呼び出す
lua_table, point = move_user.game(Map, user.lua_position, commands)
...
end
private
def commands
Array(params[:commands])
end
end
おわりに
- ソーシャルゲームのサーバーとクライアント間の通信を削減するために、ゲームロジックの共有を試してみました。
- ゲームロジックはLua言語で記述しました。
- LuaスクリプトをRailsからよしなに使用するために、ラッパーを作成して、綺麗に呼べるようにしました。
実際にプロダクトに導入するかはおいておいて、こういう実験は楽しいですね。
タイトルにあるように、この記事は前編です。
クライアントの話と考察は 12/22 に @hareruyanosuke が書いてくれます。お楽しみに!
追記: 中編(?)が投稿されました!
まだ都度通信で消耗してるの? 〜ソシャゲの通信頻度削減の実験〜 (中編) - Qiita