ゲームのセーブ機能と言うと難しく考えがちだったりもしますが、 こだわらなければ結構カンタンです。
- 「事例のサンプル」として想定するアプリ
- 何をセーブするのか?
- まずは平文で
- ちょっとだけ「わかりにくく」する
- ダミー情報の森に隠して分かりにくくする
- 改竄防止案1:ハッシュ値を利用する
- 改竄防止案2:チェックサムを利用する
- サンプルアプリ(ダウンロード)
- まとめ
- 実例(宣伝)『祝福のナワール』
こだわらなければ、とは主に2点。「正確さ」と「改竄防止性能」。
所詮は「ゲームの進行状況のメモ」。 ゲームの全ての情報を完璧にセーブ/ロードする必要はありません。 たとえばBGMの再生位置は再現しなくてもいいですよね? キャラの向きは? ロードすると正面に戻ってしまう? 別にいいんじゃね?
あくまでゲームです。銀行の個人情報ってわけじゃありません。 改竄されたらされたで別にいいじゃないですか。 逆に、そこまで熱心にゲームを遊び尽くしてもらえるなんて制作者冥利。 一応の礼儀として「そこそこ分かりにくく」はしておきつつ、「改竄ダメ絶対!」は追求しないこととします。
ここではプログラム言語そのものの解説ではなく、セーブ機能を実現するための「考え方」を紹介します。
「考え方」ですので、どんな言語を使う場合でも応用できるはずです。 紹介するのは「テキストファイルの読み書き」さえできれば使える方法です。
と言うより、テキストファイルという「簡単に中身を見れる」データでもセーブファイルとして成立する ということがお伝えできればと思います。
事例として、次のような「ゲームっぽいもの」を想定します。
自キャラ(東北ずん子)を動かして、画面内の「ずんだ餅」を回収するゲームっぽい何かです。 シンプルですね。
さて、これにセーブ機能をつけるにはどうすればいいでしょうか?
まずは、保存する情報を決めます。
・ずんだ餅の位置(x,y)
・取得ずんだ餅の数
大体これぐらいの情報が再現できればセーブ機能として充分でしょう。
あ、それと、
も一応セーブしておくことにしましょうか。
アプリケーションの全部の情報を正確に保存する必要はありません。「ゲーム」として本質的な情報だけでオッケーです。
保存する情報が決まったら後は簡単です。
自キャラY=200
ずんだX=400
ずんだY=300
取得ずんだ数=14
↑
みたいなテキストファイルを出力すればセーブ完了。
それを読み込んでゲーム内に反映させればロード完了。
やったぜ!
これにてセーブ機能完成!ヒャッハー!
ということにしてもいいんですが、 セーブファイルをメモ帳などで開くとデータが丸分かりで改竄してくださいと言わんばかりです。 「たかがゲーム」であり、改竄を厳密に禁じる必要はないとは言え、これではちょっと興ざめです。
そもそもデータの項目名まで親切にセーブデータに含める必要はあるんでしょうか?
というわけで、純粋に「データだけ」を保存することにします。
200
400
300
14
こうするだけで、仕様を知らないプレイヤーには「分かりにくい」セーブデータになりました。 むしろプログラムを書く側としては、この方が余分な情報がないので扱いやすいですね。
これにてセーブ機能完成! ということにしても良いぐらいですね。
ただ、ここまで来たならあと一歩で簡単に「分かりにくさ」を爆上げする方法があるのでご紹介します。
ダミーのデータを大量に紛れ込ませておく。たったこれだけで分かりにくさが激増します。
例えばCSV形式でも何でもいいんですが、要素を100個とか1000個とかにしておきます。
その中に例えば:
46番目の要素……自キャラのY
・
・
(以下略)
・
・
のようにセーブ情報を紛れ込ませておきます。 「何番目の要素が何の情報なのか?」は制作者だけが知っている、というわけです。
ダミー情報の量を増やすだけで分かりにくさをカサ増しできます。
期待できる効果は2つ。
・心を折る効果
改竄しようとしても「ハズレ」る確率が高まりますので諦めさせる効果が期待できます。
・威圧効果
好奇心から「ちょっと改竄してみようかな?」と思ってファイルを開いたチャレンジャーをビビらせる効果が期待できます。
これだけで一般的な個人制作規模のゲームなら充分なんじゃないでしょうか?
たとえば「点数1472」でセーブデータを保存したとします。
・セーブファイルをテキストエディターで開いて、「1472」という文字列を探す。
・ハハーン、こいつが点数ってワケやな!
・それを書き換えてゲーム内で読み込んでみると……
……というふうに割と簡単にハッキングできてしまいます。
それを防ぐために「さらに分かりにくく」するアイディアは色々あると思います。しかし、しょせんはイタチごっこ。 というわけで、ずばり「改竄されてないかどうか」を確認する方法を埋め込むことにします。
文字列を別の文字列に変換する「ハッシュ化」というテクノロジーを人類は発明しました。
代表的なのはMD5。
たとえば、「あいうえお」という文字列のMD5値は「86deb27a32903da70a7b2348fcf36bc3」となります。
「あいうえお」=> 86deb27a32903da70a7b2348fcf36bc3
ハッシュ値には次の特徴があります。
・ハッシュ値から元の文字列に戻すことはできない
・元の文字列がどんなに長くても、ハッシュ値の桁数は一定
昨今、大抵の言語でこうしたハッシュ値を取得する関数やライブラリが用意されてますので:
・それを使ってセーブデータ全体のハッシュ値を取得し、
・それをセーブデータ本体の後ろにでもくっつけて保存しておき、
・ロード時に本体部分のハッシュ値を取得し、
・保存されているハッシュ値と一致しているかどうかを見る
これにより:
データ本体の側に改竄があればハッシュ値がズレるので改竄を判別できます。
保存していたハッシュ値の側に改竄があっても、不一致が起きるので改竄されたことが判別できます。
ハイテクっぽいので強力な感じもしますが、 「見るからにハッシュ値であることがバレバレ」なのが弱点です。
「おっ、ハッシュ値やな! ハハーン、さてはコイツで改竄検出しとるわけやな! よっしゃ! ほんなら……」
と、データ改竄後に改竄挑戦者が自分でMD5を計算して、元のデータのMD5部分に上書きしておけばハッキング成功となります。
これを防ぐには例えば「他にもハッシュ値っぽいダミーデータを複数作って紛れ込ませておく」 「ハッシュ値を細かく分割してセーブデータ全体に散らしておく」 などの方法が考えられます。
要するに、いかに分かりにくくするかということですが、その分、作るのは面倒になります。 そのエネルギーをゲーム本体の開発に向けた方がいいかもしれません。 どこまで改竄防止パワーを追求するかは制作プロジェクトの方針次第ということになろうかと思います。
ハッシュ値の簡易版みたいなものです。
たとえばデータの要素数が(ダミーを含めて)100個あるとします。 それを全部足した数字をどこかに含めておきます。 この数字を「チェックサム」と言います。
ロードの際も同様に読み込んだデータを全部足した数字を算出します。 それを保存されているチェックサムと比較し、一致していれば改竄なし。不一致なら改竄あり、と判定します。
ハッシュと比較した場合のメリットとしては、 外部のライブラリが不要であることと、単なる数字なので「見るからにハッシュ値」になることは避けやすいこと。
3つあります:
・単に合計するだけだと異様にデカい数字になって「チェックサムだ!」とバレる
・全部のデータを数値化しないといけない(テキストデータはどうする?)
チェックサム自体もセーブデータの中に含めるわけですが、 そうするとチェックサムを追加したことによってチェックサムが変わってしまう。
そのため、チェックサムは除外してチェックサムを計算するように実装する必要があります。
たとえば、「72番目の要素をチェックサムの保存に使う」と決めておいて、 チェックサムの算出時には72番目の要素を除外して計算することとしましょう。
これはさきほどのハッシュ値の欠点と同じですね。 しかしチェックサムの方が扱いは容易です。 要素を合計した数字をそのまま使うのではなく、たとえば「100で割った余り」のようにするとよいかなと思います。 要するに、他のデータ(ダミー含む)と見比べて見分けがつきにくい程度の桁数に変換すればオッケーということです。
かと言って例えば1桁にまで圧縮すると10分の1の確率で改竄が突破できるようになりますので、そこは適当に。 あるいはチェックサムを「前半」「後半」など複数に分割するといった手もあろうかと思います。
これも結局、改竄されにくさをどこまで追求するか(しないか)は、制作者である自分自身の考え方次第 ということになろうかとは思います。
「自キャラの位置」や「取得ずんだ数」のような数値データだけであれば問題ありませんが、 数値ではない情報が混ざっている場合はどうするか? 例えば今回の例では「自キャラの向き」が該当します。
向きを「UP」「DOWN」「LEFT」「RIGHT」のような文字列にしていると計算できなくなりますが、 これをそもそも「1」「2」「3」「4」のような数字にしておけば計算可能です。 そのままではソースコードの可読性が下がりますので、 定数を定義しておくといった工夫をしておくと良いかもしれません。
最初から定数を使ってコーディングしていた場合はいいですが、
適当に文字列で処理を書いちゃってたんだよね〜、という場合もあろうかと思います。
今さらソースを書き直すのは面倒くさい。
そういう場合はセーブ/ロード処理の箇所でだけ、
たとえば「UP」は「1」、「DOWN」は「2」のように適当に規則を作って数字に変換する処理を入れておけば完璧です。
「向き」のような単純な情報であればプログラム的に文字列である必要がないので数値化も簡単ですが、 文章のようなデータを保存したい場合はどうするか? その箇所だけさきほど紹介したMD5のようなハッシュ値で改竄をチェックすることとし…… と思ったのですが、ゲームのセーブデータとして自由度の高い長文を保存する案件ってあんまりないんじゃないですかね? プレイヤーが入力する文字列があるとすれば、キャラ名ぐらいかな?
キャラ名ぐらいなら改竄されてもゲームに影響ないってことで、 その項目はチェックサムの計算から除外しておいてもいいんじゃないですかね? どうしてもゲーム途中で変更されると困る場合は、一文字ずつ文字コードに置き換えて……という感じですかね。
「絶対に改竄不可能なセーブ機能」を追求する必要があるのかどうか? 目的は「あくまでゲーム用途」ですので、あんまりこだわりすぎないのも重要ではないかな〜と先生は思います(誰が先生だ)。
では、これまでの内容を元に、最初にお見せした「ゲームのような何か」にセーブ機能をつけてみましょう。
言語はLuaで実行環境はLÖVEとなっております。
当記事はLuaやLÖVEの解説記事ではないので、細かい説明は省きます。
ソースコードのコメントを見て、実装例の参考にしていただければと思います。
下記のサンプルでは改竄検出にはチェックサム方式を使っています。
以下、セーブ機能(toSave関数)とロード機能(toLoad関数)の箇所のみ掲載します。 ソースコード全体は下記のリンクでダウンロードできます。
Windowsでの動作を想定しています。
動かし方は同梱のReadme.txtをご参照ください。
キーボードの1〜5キーを押すとそのスロットにセーブします。
Shiftを押しながら1〜5キーを押すとロードします。
(あくまでセーブ/ロード機能だけのサンプルですので、GUIはつけてません)
人が自由になれるといいなぁ、と思います。
いや、人はもともと自由なのではあります。
今回紹介したのは「考え方」です。特定の言語には依存しない方法です。
ゲームにとってコンピューターはゲームをするための道具の1つです。
セーブ機能の役割は「ゲームの進行状況のメモ」の一種です。
改竄防止はプレイヤーの「遊び方」との付き合い方の一種です。
昨今、「ゲームを作るためのツール」が充実してきており、 プログラミングをする場面は減ってきているようにも思いますが、 その反面、「特定のツールを買うなどしてそのツールの使い方を覚えなければゲームが作れない」 かのようなイメージが強まっている面もなきにしもあらずな気もします。
今回紹介した方法も1つの「考え方」に過ぎません。
自由な発想で、作りたいものを作りたいように作るための一助になれば幸いです。
今回紹介したのと同じ方法でセーブ機能を作ったゲームです。 ソースコードも公開してます。
改竄検出はしてません。ソースコードも公開してるので、どの要素が何のデータなのかも丸わかりという親切設計。 ゲームに飽きたらハッキングしてみるのも楽しいかも。