ランキングシステム

先日、ランキングシステムを搭載したFlashゲームを制作した。→ドリルナ
明らかにランキングシステムは誰もが作り使ってるはずなのだが、そのサンプルコードがどこにも見当たらないのは困った。よってぼくはPHPを一から勉強して幾千幾万と書かれたであろう車輪コードを再発明して実装したのだった。
このめんどくささと来たらなかったのでコードをここに載せておく。

ソースコードのライセンスはこちら
※ 本記事ではFlash(Action Script 3)からの接続について説明する。他言語からの接続も応用すればきっと出来るハズ…。

ファイル

rankingsystem.zip

仕組み

  1. FlashからPHPを呼び出す。
  2. PHPMySQLのデータベースを叩く。
  3. 結果をFlashに戻す。

PHPにPOSTで通信できればFlash以外からでも可能。

導入

ローカルに環境を構築したいならXAMPPというものを入れると一発。サーバに入れるときはyumコマンドなりなんなりで。ググれば山ほど資料が出てくる。ココとか
PHPは5.0以上必須。まあ5.0未満を探す方が難しいくらいだと思うので特に問題なし。

  • データベースを作る

phpMyAdminとかでrankingdbというデータベースにscorerankingというテーブルを作る。
カラムは四つ。

フィールド 種別 インデックス
username varchar(32)
userscore int(11) INDEX
date date
userhash varchar(20)

インデックスはuserhashもあったほうが良いのかも。分からない。

  • ファイルをぶち込んでswfを開く。

xamppの場合htdocsフォルダに入れた後http://localhost/からアクセスしないと動作しないので注意。

PHP部分の解説

highscorelist.phpは極単純な機能三つのみを内包した単純なPHPスクリプトである。
PHP5.0から標準搭載されたpdoを用いてMySQLデータベースにアクセスを行う。

$Mode = $_POST["SendMode"];

SendModeで受け取った値で動作が分岐する。Mode==1はランキングの一覧を取得、Mode==2はランキングの追記、Mode==3はランキングの下限スコア(100位)を取得する。

  • ランキング一覧取得
/* ランキング一覧表示 */
$result_name = "";
$result_score = "";
$sql = 'select username,userscore from scoreranking order by userscore desc limit 100;';
$ResultArray = $dbh->query($sql);
foreach ($ResultArray as $row) {
	$result_name .= $row['username']."\n";
	$result_score .= $row['userscore']."\n";
}
echo "comp=".$result_name."<>".$result_score;
break;

SELECTクエリを実行し、ユーザーネームとスコアに分けてリザルトに挿入する。二つの間には区切り記号『<>』を挟み、Flash側で分解する。ここら辺は自由に整形しても良い。
echoの後の値がPHPからFlashへの返り値となる。

  • ランキング追記
/* ランキングにスコア挿入 */
$today = getdate();
$todaydate = date("Y-m-d");

$sql_deleta = $dbh->prepare("DELETE FROM scoreranking WHERE userhash = :USERHASH;");
$sql_deleta->bindValue(':USERHASH',$playerhash);
$sql_deleta->execute();

$sql_inserta = $dbh->prepare("INSERT INTO scoreranking(username,userscore,date,userhash) VALUES(:USERNAME,:USERSCORE,:DATE,:USERHASH);");
$sql_inserta->bindValue(':USERNAME',$playername);
$sql_inserta->bindValue(':USERSCORE',$score);
$sql_inserta->bindValue(':DATE',$todaydate);
$sql_inserta->bindValue(':USERHASH',$playerhash);
$sql_inserta->execute();

echo "comp=COMPLETED";	
break;

入力されたスコアをランキングに挿入する。この時、ユーザ識別文字列『userhash』が同一のものがあれば、それを先に削除する。詳しくはFlash部分解説にて後述。一覧表示で日付を求めてないため、ぶっちゃけ日付は要らない。
恐らくUPDATEでも行けるが何となくDELETE&INSERTで実装している。
返り値は完了を示す文字列のみ。

  • ランキング下限スコア取得
/* ランキングの下限スコア取得 */
$result = 0;

$sql = $dbh->prepare("SELECT username FROM highscore ORDER BY userscore DESC LIMIT 99,1;");
$sql->execute();
$result = $sql->fetchColumn(1);
if ($result=='') $result = 1;

echo "comp=".$result;
break;

100位のスコアを取得する。存在しない場合は0。
何の目的かというと、いちいちデータベースにアクセスしてハイスコアがランキングに入るかどうかチェックすると負荷がかかるため、予め100位のスコアを取得し、それ以上のハイスコアでなければアクセス自体しないという仕様にするためである。
返り値にはスコアだけが入る。

PHP部分はこんな感じである。

Flash部分の解説

  • クラス「CScoreDeliver」について

スコアを受け渡したり取ってきたりするクラス。予めユーザネームやスコアを登録しておくと、後はCallScoreRanking関数を呼ぶだけで自動的にそれらの値を使ってあれこれしてくれる。
結果はm_Resultに収納され、m_Completeがオンになるので、IsComplete関数とGetResult関数を組み合わせれば効率的に結果を取得することが出来る。(IsComplete関数は毎フレーム呼び出すことになるだろう)
また、今回はぼくの個人的な好みでこういう仕様にしたが、CScoreDeliver自体をEventDispatcherのサブクラスにすれば、CScoreDeliverのonCompleteをイベントリスナーに登録することも可能。

結果は整形しないので別途呼び出し側で弄る必要がある。

  • onComplete
private function onComplete(event:Event):void {
	var res:URLVariables = new URLVariables( event.target.data );  
	m_Result = res.comp;
	m_Complete = true;
}

compというのはPHPパートで返り値として用いられていた"comp="のソレである。返り値を工夫すれば複数の値を受け取ることも可能。

  • CreateUserHash
public function CreateUserHash():String {
	var hsnum:int = 0;
	var unlength:int = m_UserName.length;
	var dt:Date = new Date();
	for(var i:int = 0;i<unlength;i++) hsnum += Number(m_UserName.charCodeAt(i));
	m_UserHash = hsnum.toString() + dt.getTime().toString();
	return m_UserHash;
}

呼ぶことでUserHashを作成し登録する。UserHashはランキングに登録されたユーザを一意に識別するための文字列で、ハッシュという名前が付いているが厳密にはハッシュではない。
サンプルでは極単純なハッシュ生成器が用いられているため、より耐衝突性を高めたい場合は名前を全て文字コードに変換した上で直接接続し、まとめて36進数などに変換してしまうと良いだろう。ただしデータベースのカラムはもう少し大きいものが必要となる。現存する一方向ハッシュ関数を用いても良い。(ただし原像計算の困難性は求める必要が無いので省略可能)
UserHashはSharedObjectなどで持っておくと次回以降のプレイでスコアが更新されても効率よく書き換えることが出来る。

  • CallScoreRanking呼び出し
var ScoreDeliver:CScoreDeliver = new CScoreDeliver();
var UHASH:String = ScoreDeliver.CreateUserHash();
stage.addEventListener(Event.ENTER_FRAME, MainLoop);
function MainLoop(evt:Event):void  {
	if (ScoreDeliver.IsComplete()) {
		var res:String = ScoreDeliver.GetResult();
		var Ranking:Array = res.split("<>");
		tx_result_name.text = Ranking[0];
		tx_result_score.text = Ranking[1];
		tx_result_rankno.text = "";
		for(var i:int=1;i<=100;i++) tx_result_rankno.appendText(i+"\n");
	}
}

testflash.flaに書かれている呼び出し側の処理。
ScoreDeliverインスタンスを作成し、UserHashを生成し、IsCompleteを定期的に呼び出すMainLoopをイベントリスナーに登録する。渡されたResultを<>で分割し、それぞれのテキストフィールドに振り分けている。

ScoreDeliver.CallScoreRanking(1);

あとはScoreDeliverインスタンスのCallScoreRankingを呼び出す。引数はSendMode。スコア送信時はスコアの登録を忘れずに。

まとめ

こんな感じである。コード自体はかなり単純なので、恐らくPHPSQLが分からない人にも理解できると思われる。というか自分がよくわかってない。