TOP > プログラム関連 > Lua > LÖVE(Love2D) > セーブ機能の作り方

◆◆ ゲームプログラミング ◆◆
「超カンタン!」〜セーブ機能の作り方〜

ゲームのセーブ機能と言うと難しく考えがちだったりもしますが、 こだわらなければ結構カンタンです。

  • 「事例のサンプル」として想定するアプリ
  • 何をセーブするのか?
  • まずは平文で
  • ちょっとだけ「わかりにくく」する
  • ダミー情報の森に隠して分かりにくくする
  • 改竄防止案1:ハッシュ値を利用する
  • 改竄防止案2:チェックサムを利用する
  • サンプルアプリ(ダウンロード)
  • まとめ
  • 実例(宣伝)『祝福のナワール』
2つの「こだわらなければ」

こだわらなければ、とは主に2点。「正確さ」「改竄防止性能」

「正確さ」にこだわらない

所詮は「ゲームの進行状況のメモ」。 ゲームの全ての情報を完璧にセーブ/ロードする必要はありません。 たとえばBGMの再生位置は再現しなくてもいいですよね? キャラの向きは? ロードすると正面に戻ってしまう? 別にいいんじゃね?

「改竄防止性能」にこだわらない

あくまでゲームです。銀行の個人情報ってわけじゃありません。 改竄されたらされたで別にいいじゃないですか。 逆に、そこまで熱心にゲームを遊び尽くしてもらえるなんて制作者冥利。 一応の礼儀として「そこそこ分かりにくく」はしておきつつ、「改竄ダメ絶対!」は追求しないこととします。

この記事の方針

ここではプログラム言語そのものの解説ではなく、セーブ機能を実現するための「考え方」を紹介します。

「考え方」ですので、どんな言語を使う場合でも応用できるはずです。 紹介するのは「テキストファイルの読み書き」さえできれば使える方法です。

■ テキストかバイナリか?
テキストファイルの方が言語や環境を選ばずに扱えるケースが多い(ような気がする) ので、この記事ではテキストファイルを使う前提で説明を進めますが、 バイナリファイルでも理屈は同じです。
と言うより、テキストファイルという「簡単に中身を見れる」データでもセーブファイルとして成立する ということがお伝えできればと思います。
「事例のサンプル」として想定するアプリ
↑目次へ↑

事例として、次のような「ゲームっぽいもの」を想定します。

自キャラ(東北ずん子)を動かして、画面内の「ずんだ餅」を回収するゲームっぽい何かです。 シンプルですね。

さて、これにセーブ機能をつけるにはどうすればいいでしょうか?

何をセーブするのか?
↑目次へ↑

まずは、保存する情報を決めます。

・自キャラの位置(x,y)
・ずんだ餅の位置(x,y)
・取得ずんだ餅の数

大体これぐらいの情報が再現できればセーブ機能として充分でしょう。

あ、それと、

・自キャラの向き

も一応セーブしておくことにしましょうか。

大事なこと

アプリケーションの全部の情報を正確に保存する必要はありません。「ゲーム」として本質的な情報だけでオッケーです。

まずは平文で
↑目次へ↑

保存する情報が決まったら後は簡単です。

自キャラX=300
自キャラY=200
ずんだX=400
ずんだY=300
取得ずんだ数=14


みたいなテキストファイルを出力すればセーブ完了。 それを読み込んでゲーム内に反映させればロード完了。

やったぜ!

これではちょっと「わかりやすすぎ」る

これにてセーブ機能完成!ヒャッハー!

ということにしてもいいんですが、 セーブファイルをメモ帳などで開くとデータが丸分かりで改竄してくださいと言わんばかりです。 「たかがゲーム」であり、改竄を厳密に禁じる必要はないとは言え、これではちょっと興ざめです。

ちょっとだけ「わかりにくく」する
↑目次へ↑

そもそもデータの項目名まで親切にセーブデータに含める必要はあるんでしょうか?

というわけで、純粋に「データだけ」を保存することにします。

300
200
400
300
14

こうするだけで、仕様を知らないプレイヤーには「分かりにくい」セーブデータになりました。 むしろプログラムを書く側としては、この方が余分な情報がないので扱いやすいですね。

これにてセーブ機能完成! ということにしても良いぐらいですね。

ただ、ここまで来たならあと一歩で簡単に「分かりにくさ」を爆上げする方法があるのでご紹介します。

ダミー情報の森に隠して分かりにくくする
↑目次へ↑

ダミーのデータを大量に紛れ込ませておく。たったこれだけで分かりにくさが激増します。

795,699,591,754,81,662,340,736,427,569,115,55,975,706,176,975,277,355,473,680,576,797,452,183,660,212,597,145,830,584,25,837,50,830,627,827,181,256,922,317,79,355,922,26,677,745,554,118,134,34,500,690,507,856,140,294,878,766,696,649,839,11,54,770,405,960,987,385,295,272,885,957,594,759,62,363,641,948,750,102,177,52,223,148,429,603,272,826,286,202,150,768,52,804,690,145,937,809,387,797

例えばCSV形式でも何でもいいんですが、要素を100個とか1000個とかにしておきます。
その中に例えば:

12番目の要素……自キャラのX
46番目の要素……自キャラのY


(以下略)


のようにセーブ情報を紛れ込ませておきます。 「何番目の要素が何の情報なのか?」は制作者だけが知っている、というわけです。

・大量の要素の中の一部の要素だけが本物
795,699,591,754,81,662,340,736,427,569, 115,55,975,706,176,975,277,355,473,680, 576,797,452,183,660,212,597,145,830,584, 25,837,50,830,627,827,181,256,922,317, 79,355,922,26,677,745,554,118,134,34, 500,690,507,856,140,294,878,766,696,649, 839,11,54,770,405,960,987,385,295,272, 885,957,594,759,62,363,641,948,750,102, 177,52,223,148,429,603,272,826,286,202, 150,768,52,804,690,145,937,809,387,797

ダミー情報の量を増やすだけで分かりにくさをカサ増しできます。

期待できる効果は2つ。

・心を折る効果
改竄しようとしても「ハズレ」る確率が高まりますので諦めさせる効果が期待できます。

・威圧効果
好奇心から「ちょっと改竄してみようかな?」と思ってファイルを開いたチャレンジャーをビビらせる効果が期待できます。

これだけで一般的な個人制作規模のゲームなら充分なんじゃないでしょうか?

それでも改竄は可能

たとえば「点数1472」でセーブデータを保存したとします。

・セーブファイルをテキストエディターで開いて、「1472」という文字列を探す。
・ハハーン、こいつが点数ってワケやな!
・それを書き換えてゲーム内で読み込んでみると……

……というふうに割と簡単にハッキングできてしまいます。

それを防ぐために「さらに分かりにくく」するアイディアは色々あると思います。しかし、しょせんはイタチごっこ。 というわけで、ずばり「改竄されてないかどうか」を確認する方法を埋め込むことにします。

改竄検出案1:ハッシュ値を利用する
↑目次へ↑

文字列を別の文字列に変換する「ハッシュ化」というテクノロジーを人類は発明しました。
代表的なのはMD5。

たとえば、「あいうえお」という文字列のMD5値は「86deb27a32903da70a7b2348fcf36bc3」となります。

文字列「あいうえお」のMD5

「あいうえお」=> 86deb27a32903da70a7b2348fcf36bc3

ハッシュ値には次の特徴があります。

・元の文字列が少しでも違うと、ハッシュ値も別のものになる
・ハッシュ値から元の文字列に戻すことはできない
・元の文字列がどんなに長くても、ハッシュ値の桁数は一定

昨今、大抵の言語でこうしたハッシュ値を取得する関数やライブラリが用意されてますので:

・それを使ってセーブデータ全体のハッシュ値を取得し、
・それをセーブデータ本体の後ろにでもくっつけて保存しておき、
・ロード時に本体部分のハッシュ値を取得し、
・保存されているハッシュ値と一致しているかどうかを見る

これにより:

データ本体の側に改竄があればハッシュ値がズレるので改竄を判別できます。
保存していたハッシュ値の側に改竄があっても、不一致が起きるので改竄されたことが判別できます。

ハッシュ値の欠点

ハイテクっぽいので強力な感じもしますが、 「見るからにハッシュ値であることがバレバレ」なのが弱点です。

「おっ、ハッシュ値やな! ハハーン、さてはコイツで改竄検出しとるわけやな! よっしゃ! ほんなら……」

と、データ改竄後に改竄挑戦者が自分でMD5を計算して、元のデータのMD5部分に上書きしておけばハッキング成功となります。

これを防ぐには例えば「他にもハッシュ値っぽいダミーデータを複数作って紛れ込ませておく」 「ハッシュ値を細かく分割してセーブデータ全体に散らしておく」 などの方法が考えられます。

要するに、いかに分かりにくくするかということですが、その分、作るのは面倒になります。 そのエネルギーをゲーム本体の開発に向けた方がいいかもしれません。 どこまで改竄防止パワーを追求するかは制作プロジェクトの方針次第ということになろうかと思います。

改竄検出案2:チェックサムを利用する
↑目次へ↑

ハッシュ値の簡易版みたいなものです。

たとえばデータの要素数が(ダミーを含めて)100個あるとします。 それを全部足した数字をどこかに含めておきます。 この数字を「チェックサム」と言います。

ロードの際も同様に読み込んだデータを全部足した数字を算出します。 それを保存されているチェックサムと比較し、一致していれば改竄なし。不一致なら改竄あり、と判定します。

ハッシュと比較した場合のメリットとしては、 外部のライブラリが不要であることと、単なる数字なので「見るからにハッシュ値」になることは避けやすいこと。


ダミーを含めずに実データだけのチェックサムでも良さそうに思えますが、 ダミーを含めておくことで「どこを改竄してもエラー」になるため、 改竄挑戦者の心を折る効果が期待できます。

実装上の課題

3つあります:

・チェックサムを追加したことによってチェックサムが変わってしまう
・単に合計するだけだと異様にデカい数字になって「チェックサムだ!」とバレる
・全部のデータを数値化しないといけない(テキストデータはどうする?)

チェックサムを追加したことによってチェックサムが変わってしまう件

チェックサム自体もセーブデータの中に含めるわけですが、 そうするとチェックサムを追加したことによってチェックサムが変わってしまう。

そのため、チェックサムは除外してチェックサムを計算するように実装する必要があります。

たとえば、「72番目の要素をチェックサムの保存に使う」と決めておいて、 チェックサムの算出時には72番目の要素を除外して計算することとしましょう。

単に合計するだけだと異様にデカい数字になって「チェックサムだ!」とバレる件

これはさきほどのハッシュ値の欠点と同じですね。 しかしチェックサムの方が扱いは容易です。 要素を合計した数字をそのまま使うのではなく、たとえば「100で割った余り」のようにするとよいかなと思います。 要するに、他のデータ(ダミー含む)と見比べて見分けがつきにくい程度の桁数に変換すればオッケーということです。

かと言って例えば1桁にまで圧縮すると10分の1の確率で改竄が突破できるようになりますので、そこは適当に。 あるいはチェックサムを「前半」「後半」など複数に分割するといった手もあろうかと思います。

これも結局、改竄されにくさをどこまで追求するか(しないか)は、制作者である自分自身の考え方次第 ということになろうかとは思います。

全部のデータを数値化しないといけない(テキストデータはどうする?)件

「自キャラの位置」や「取得ずんだ数」のような数値データだけであれば問題ありませんが、 数値ではない情報が混ざっている場合はどうするか? 例えば今回の例では「自キャラの向き」が該当します。

解決案1:数値で表すようにしておく

向きを「UP」「DOWN」「LEFT」「RIGHT」のような文字列にしていると計算できなくなりますが、 これをそもそも「1」「2」「3」「4」のような数字にしておけば計算可能です。 そのままではソースコードの可読性が下がりますので、 定数を定義しておくといった工夫をしておくと良いかもしれません。

解決案2:セーブのときだけ数値に変換する

最初から定数を使ってコーディングしていた場合はいいですが、 適当に文字列で処理を書いちゃってたんだよね〜、という場合もあろうかと思います。 今さらソースを書き直すのは面倒くさい。

そういう場合はセーブ/ロード処理の箇所でだけ、 たとえば「UP」は「1」、「DOWN」は「2」のように適当に規則を作って数字に変換する処理を入れておけば完璧です。

解決案3:諦める

「向き」のような単純な情報であればプログラム的に文字列である必要がないので数値化も簡単ですが、 文章のようなデータを保存したい場合はどうするか? その箇所だけさきほど紹介したMD5のようなハッシュ値で改竄をチェックすることとし…… と思ったのですが、ゲームのセーブデータとして自由度の高い長文を保存する案件ってあんまりないんじゃないですかね? プレイヤーが入力する文字列があるとすれば、キャラ名ぐらいかな?

キャラ名ぐらいなら改竄されてもゲームに影響ないってことで、 その項目はチェックサムの計算から除外しておいてもいいんじゃないですかね? どうしてもゲーム途中で変更されると困る場合は、一文字ずつ文字コードに置き換えて……という感じですかね。

「絶対に改竄不可能なセーブ機能」を追求する必要があるのかどうか? 目的は「あくまでゲーム用途」ですので、あんまりこだわりすぎないのも重要ではないかな〜と先生は思います(誰が先生だ)。

サンプルアプリ
↑目次へ↑

では、これまでの内容を元に、最初にお見せした「ゲームのような何か」にセーブ機能をつけてみましょう。

言語はLuaで実行環境はLÖVEとなっております。
当記事はLuaやLÖVEの解説記事ではないので、細かい説明は省きます。 ソースコードのコメントを見て、実装例の参考にしていただければと思います。

下記のサンプルでは改竄検出にはチェックサム方式を使っています。

以下、セーブ機能(toSave関数)とロード機能(toLoad関数)の箇所のみ掲載します。 ソースコード全体は下記のリンクでダウンロードできます。

ファイル名:main.lua
--========================================================= -- セーブデータ書き込み -- 引数はスロット番号 セーブファイルのファイル名につける --========================================================= toSave = function ( slot ) -- ダミー込みで要素100個の配列を作り、ランダムな値で埋める local saveData = {} for i = 1, 100, 1 do local data = math.random ( 0,999 ) saveData [ #saveData + 1 ] = data end
--[[ 保存する情報メモ(もっと手抜きしてもいいし、もっと細かくしてもいい) ・自キャラのx,y ・自キャラの向き ・ずんだ餅のx,y ・取得ずんだ数 ・ずんだ餅出現前のインターバル(取った感を出すための演出) ・次のずんだ餅出現位置 ]]--
-- 用意しておいた配列の中に、位置を適当に決めて各情報を入れていく saveData [ 32 ] = player.x saveData [ 51 ] = player.y saveData [ 70 ] = player.direction -- ずんだ餅は取った直後は消えているので、存在の有無を確認している if ( zunda ~= nil ) then saveData [ 85 ] = zunda.x saveData [ 7 ] = zunda.y else saveData [ 85 ] = 0 saveData [ 7 ] = 0 end saveData [ 50 ] = zundaNum saveData [ 2 ] = zundaRespawnWait saveData [ 41 ] = nextZundaX saveData [ 65 ] = nextZundaY -- チェックサムを計算 local sum = 0 for i = 1, #saveData, 1 do sum = sum + saveData[i] end -- 配列の66番目の要素をチェックサムとする sum = sum - saveData[66] -- 合計から差し引いている local checkSum = sum%1000 -- 値を1000で割った余りにしている saveData[66] = checkSum -- 66番という数字に深い意味はない -- セーブファイルに書き込む文字列を作っていく local saveDataSerial = "" -- セーブ情報を書き込んだ配列を "," で区切った文字列にしている for i = 1, #saveData, 1 do saveDataSerial = saveDataSerial..saveData[i] if ( i < #saveData ) then saveDataSerial = saveDataSerial.."," end end -- ファイルに出力している utils.writeFile ( "save"..slot, saveDataSerial ) -- セーブしましたというメッセージを画面に表示する showMessage ( "セーブ [" .. slot .. "]" ) end --========================================================= -- セーブデータ読み込み -- 引数はスロット番号 --========================================================= toLoad = function ( slot ) -- 指定されたスロット番号のセーブファイルを読み込む local saveDataSerial = utils.readFile ( "save"..slot ) -- "," 区切りの文字列(の筈)なので、それを配列に戻す local saveData = utils.split ( saveDataSerial, ",") -- 要素数は100個のはず。そうじゃないならエラーを出す(→else節へ) if ( #saveData == 100 ) then -- チェックサムを確認 -- 読み込んだセーブデータのチェックサムを計算 local sum = 0 for i = 1, #saveData, 1 do sum = sum + tonumber(saveData[i]) end sum = sum - tonumber(saveData[66]) -- 66番目の要素がチェックサム local checkSum = sum%1000 -- 合計を1000で割った余りがチェックサム -- チェックサムが不一致だったらエラーを出す if ( tonumber(saveData[66]) ~= checkSum ) then showMessage ( "チェックサムが合わない [" .. slot .. "]" ) -- チェックサムが合っている場合はロード処理を実行 else -- セーブデータを作ったときの逆の手順。要素番号を自分で決めた仕様の通りに player.x = tonumber ( saveData [ 32 ] ) player.y = tonumber ( saveData [ 51 ] ) player.direction = tonumber ( saveData [ 70 ] ) zundaNum = tonumber ( saveData [ 50 ] ) nextZundaX = tonumber ( saveData [ 41 ] ) nextZundaY = tonumber ( saveData [ 65 ] ) zundaRespawnWait = tonumber ( saveData [ 2 ] ) -- ずんだ餅が出現待ちの状態かどうか? if ( zundaRespawnWait > 0 ) then zunda = nil -- ずんだ無き世界とする -- ずんだ餅を再現 else zunda = Zunda.new ( { x = tonumber(saveData[85]), y = tonumber(saveData[7]) }) end -- ロード処理が完了した旨のメッセージを画面に表示している showMessage ( "ロード [" .. slot .. "]" ) end -- セーブデータがおかしかった場合のエラーメッセージ else showMessage ( "セーブデータが存在しないか壊れています [" .. slot .. "]" ) end end
ダウンロード

saveTest.zip

Windowsでの動作を想定しています。
動かし方は同梱のReadme.txtをご参照ください。

実演動画

キーボードの1〜5キーを押すとそのスロットにセーブします。
Shiftを押しながら1〜5キーを押すとロードします。
(あくまでセーブ/ロード機能だけのサンプルですので、GUIはつけてません)

まとめ
↑目次へ↑

人が自由になれるといいなぁ、と思います。
いや、人はもともと自由なのではあります。

今回紹介したのは「考え方」です。特定の言語には依存しない方法です。

ゲームにとってコンピューターはゲームをするための道具の1つです。
セーブ機能の役割は「ゲームの進行状況のメモ」の一種です。
改竄防止はプレイヤーの「遊び方」との付き合い方の一種です。

昨今、「ゲームを作るためのツール」が充実してきており、 プログラミングをする場面は減ってきているようにも思いますが、 その反面、「特定のツールを買うなどしてそのツールの使い方を覚えなければゲームが作れない」 かのようなイメージが強まっている面もなきにしもあらずな気もします。

今回紹介した方法も1つの「考え方」に過ぎません。
自由な発想で、作りたいものを作りたいように作るための一助になれば幸いです。

実例(宣伝)『祝福のナワール』
↑目次へ↑

今回紹介したのと同じ方法でセーブ機能を作ったゲームです。 ソースコードも公開してます。


改竄検出はしてません。ソースコードも公開してるので、どの要素が何のデータなのかも丸わかりという親切設計。 ゲームに飽きたらハッキングしてみるのも楽しいかも。




    TOP > プログラム関連 > Lua > LÖVE(Love2D) > セーブ機能の作り方