ホーム < ゲームつくろー! < SQLite超入門編

その4 SQLiteでRPGのアイテムを管理してみよう


 前章でSQLiteライブラリの基本的な扱いを見てきました。思ったよりもずっと簡単で嬉しいライブラリです。本章では早速SQLiteを用いてデータベースから色々な情報を取り出してみたいと思います。例としてRPGのアイテムを操作をしてみます。



@ アイテムテーブル

 今回は武器と防具、道具の3種類のアイテムを1つのテーブルで管理します。1つのアイテムには次のような属性があるとしましょう:

アイテムテーブル
カラム名 ID Name Type Price Comment
説明 固有ID アイテムの名前 武具か防具か道具か お値段 アイテムの説明文章

また現在持っているアイテムはストックテーブルとして管理します:

ストックテーブル
カラム名 ID Stock
説明 固有ID 保有している数


多分実際のゲームではもっともっと属性がありますが、データベースを使ってみるならこれで十分です。



A アイテムDBファイルの作成

 まずは上記2つのテーブルを格納するItem.dbデータベースファイルをsqlite3_open関数で新規作成しましょう:

データベースファイルの新規作成
sqlite3 *ItemDB = 0;
int rc = sqlite3_open( "Item.db", &ItemDB );
if ( rc != SQLITE_OK ) {
   return 0;
}

続いてアイテムテーブルとストックテーブルをそれぞれSQLのCREATE文で作成します:

テーブルの作成
// アイテムテーブルとストックテーブルの作成
// アイテムテーブル生成SQL
char ItemTable_Create_SQL[] = "CREATE TABLE ItemTable ("
                              "               ID      INTEGER PRIMARY KEY,"
                              "               Name    TEXT    NOT NULL   ,"
                              "               Type    TEXT    NOT NULL   ,"
                              "               Price   INTEGER NOT NULL   ,"
                              "               Comment TEXT               )";

// ストックテーブル生成SQL
char StockTable_Create_SQL[] = "CREATE TABLE StockTable ("
                               "              ID      INTEGER PRIMARY KEY,"
                               "              Stock   INTEGER NOT NULL    )";

// 生成
sqlite3_exec( ItemDB, ItemTable_Create_SQL, 0, 0, &errMsg );
sqlite3_exec( ItemDB, StockTable_Create_SQL, 0, 0, &errMsg );

sqlite3_close( ItemDB );

これでデータベースファイルの元は出来上がりです。



B アイテムの登録

 続いてアイテムデータベース内のアイテムテーブルに対してアイテムの登録を行います。アイテムテーブルはRPGで使われるアイテムの種類を管理するテーブルなので、これも一度作成すれば良いだけです。

 前章ではレコードの登録をプログラム上から行いました。しかし大量にあるアイテムはファイルから情報を読み取って登録した方が良さそうです。バイナリ形式であるSQLiteのデータベースファイルを直接作る事は無理なので、タブ区切り方式でアイテム一覧をテキストファイルで作り、それをプログラム上で読み込んでデータベースに移す事にしましょう。

 タブ区切りやスペース区切りのテキストデータを作る時に最適なのがExcelです。今回は次のようなファイルを作りました:

アイテムCSVファイル
// アイテム一覧
ID Name Type Price Comment
0 木の棒 武器 100 木製の棒。折れるかもしれません。
1 鉄の棒 武器 600 鉄製の棒。頑丈ですが重いです。
2 鋼の剣 武器 1500 鋼の硬さを持つ剣。
200 木の鎧 防具 300 木製の鎧。割れるかもしれません。
201 鉄の鎧 防具 1000 鉄製の鎧。頑丈ですがかなり重いです。
202 鋼の鎧 防具 2400 鋼の硬さを持つ鎧。
1001 薬草 道具 40 HPを少し回復します。
1002 毒消し草 道具 45 毒を治癒します。
1003 火薬の袋 道具 90 敵1体に火属性の小攻撃。
1004 風の護符 道具 90 風の属性力を増幅させます。

 こういうスペースやタブで区切ったファイルをプログラムから読み込むのはSTLのfstreamの得意分野です。読み込みから登録作業までは次のようになります:

データベースにアイテムを登録
// データベースにアイテムを登録
char Insert_Item_SQL[] = " INSERT INTO ItemTable ( ID, Name, Type, Price, Comment )"
                         "             values( %d, '%s', '%s', %d, '%s' )          ";

fstream ifs( "Item.txt" );
if ( !ifs.is_open() ) {
   sqlite3_close( ItemDB );
   return 0;
}
char dummy[100], zName[24], zType[10], zComment[128], InsertSQL[512];
int iID, iPrice;
ifs.getline( dummy, 100 ); // コメント無視
ifs.getline( dummy, 100 ); // 列名無視
while(1) {
   ifs >> iID >> zName >> zType >> iPrice >> zComment;
   if( ifs.eof() )
      break;
   // データベースに登録
   sprintf( InsertSQL, Insert_Item_SQL, iID, zName, zType, iPrice, zComment );
   sqlite3_exec( ItemDB, InsertSQL, 0, 0, &errMsg );
}

登録用のSQL文は前章で紹介した書式を参照すれば簡単です。ifstream::eofメソッドの位置に注意です。本当はwhile文の判定部分に入れたいのですが、それだと最後の空読みが登録されてしまうのでNGです。



C 登録数を抽出しよう

 さて、このように登録すると「本当に登録されたのかしら?」と心配になります。そこで、確認のために登録数を抽出するSQLをデータベースに発行してみましょう。今回は抽出結果を受けないといけないためコールバック関数が必要になります(この辺りの話が怪しい人は前章を再度参照してください)。

 抽出したテーブルのレコード数をカウントするにはCOUNT命令をSELECT文で指定します。典型的な書き方は以下のような感じです:

テーブル数をカウントするSQL
char count_SQL[] = "SELECT COUNT(*) FROM ItemTable";

これをデータベースに適用します:

テーブル数をカウントするSQLを実行
int iCount = 0;
sqlite3_exec( ItemDB, count_SQL, CountCallback, &iCount, &errMsg );

ポイントは太文字部分です。今回は抽出作業なのでそれを受けるコールバック関数が必要です。またコールバックで受け取った数を呼び出し側に返すために第4引数にiCountという変数のアドレスを渡しています。

 コールバック関数は次のようにします:

カウント数を受けるコールバック関数
int CountCallback( void* pOutCount, int size, char **rec, char **ColName ){
   int *count = (int*)pOutCount;
   *count = atoi( *rec );
   return 0;
}

void*がたであるpOutCountには直接値を代入できないので、一度int型のポインタ変数に変換して、そこに抽出結果である「テーブル数」を渡します。

 先ほどの登録作業後にカウントすると「10」と抽出できました。よかった、ちゃんと登録できているようです。



D 値段の安い順の武器リストを得る

 武器屋などで買い物をする時、値段の安い順に武器を出したいとしましょう。データベースからこれを抽出するには、以下の2つの条件が必要になります;

・ 武器である!
・ 安い順

これを実現するSQLを考えて見ましょう。まず、抽出なので「SELECT」が付きます。その後には抽出する列を列挙します。今回は名前と値段、コメントを抽出します。つまり「Name, Price, Comment」と続けます。これらの列はItemTableにありますので、さらに「FROM ItemTable」とすると、指定した列全部となります。

 続いて条件文を書きます。今回の条件の1つは「武器である!」です。これはType列が武器であれば抽出対象としたいわけです。これは「WHERE文」を使います。WHERE分の後ろには抽出文を書くことができます。「ItemTable内のTypeが「武器」であれば抽出」と表現する時には「WHERE ItemTable.Type = '武器'」とします。文字列なのでシングルクォーテーションが付きます。

 ここまでで武器だけが抽出されます。そこにさらに条件として「安い順番に」を考慮しようとしています。何らかの順番で並べられたデータを抽出したい場合は「ORDER BY」という命令を使います。「〜によって並べられた」という意味ですね。さらに今回は安い順、つまり「昇順」です。昇順はASC(Asceding order)、降順はDESC(Descending order)を付記します。つまり「値段の安い順に」とする場合は「ORDER BY ItemTable.Price ASC」と書きます。

 まとめると、「ItemTableからName, Price, Comment列を選択します。条件は武器で値段の安い順。」というSQLは、

「SELECT Name, Price, Comment FROM ItemTable WHERE ItemTable.Type = '武器' ORDER BY ItemTable.Price ASC」

です。これをsqlite3_exec関数に渡します。

 もう1つ、抽出したデータを受ける構造体とコールバック関数を設けます。これらは以下のようにしました:

構造体とコールバック関数
struct ITEMINFO {
   string Name;
   int Price;
   string Comment;
};

int ItemCallback( void* iteminfo, int size, char** data, char **ColName ) {
   // 引数はリスト
   list<ITEMINFO> *pItemList = (list< ITEMINFO >*)iteminfo;
   ITEMINFO item;
   item.Name = data[0];        // Name
   item.Price = atoi(data[1]); // Price
   item.Comment = data[2];     // Comment
   pItemList->push_back( item );
   return 0;
}

SQLの命令発行部分はこんな感じです:

武器を安い順に抽出するSQL発行
// 値段の安い順に武器を抽出するSQL
char weapon_desc_SQL[] = "SELECT Name, Price, Comment FROM ItemTable "
                         "WHERE ItemTable.Type = '武器' ORDER BY ItemTable.Price ASC";
list<ITEMINFO> ItemList;
sqlite3_exec( ItemDB, weapon_desc_SQL, ItemCallback, &ItemList, &errMsg );

実際にこれを実行すると次のようにアイテムがリストに並びます:

アイテム 値段 コメント
木の棒 100 木の棒。折れるかもしれません。
鉄の棒 600 鉄製の棒。頑丈ですが重いです。
鋼の剣 1500 鋼の硬さを持つ剣。



E 道具を買おう

 続いてアイテムを指定数だけ買ってみます。ただ、データベースの話ですから、メニューからアイテムを選択して、その購入数を指定、ポチっとボタンを押しました!…の先からのお話です。

 例として「薬草」を最初に10個買ったとしましょう。これはストックテーブルに追加されます。この追加SQL文を作成してみます。テーブルにアイテムを追加するには「INSERT INTO」と「UPDATE」の2通りの道筋があります。INSERT INTOはテーブルに対してデータを「新規追加」します。一方でUPDATEはすでにテーブルにあるレコードを変更します。まずは薬草10個をレコードをに新規追加するSQLと更新するSQLの例を示します:

追加SQL
// 新規追加
char insert_stock_SQL[] = "INSERT INTO StockTable(ID, Stock) "
                          "VALUES(1001, 10)";

// 個数変更
char update_stock_SQL[] = "UPDATE StockTable SET "
                          "Stock = Stock + 10 "
                          "WHERE StockTable.ID = 1001";

 INSERT INTOの後にはデータを追加したいテーブルと追加項目を上のように記します。さらに追加するデータをVALUESで指定します。アイテムテーブルは「ID」と「ストック数」のシンプルなテーブルで、1001番は薬草のIDです。上がそのレコードを追加しようとしている事をを確認してみてください。

 続いて、更新を支持するUPDATEの後にはデータを更新したいテーブルを指定します。その後に来るのがSET命令。この命令の後に変更したい列の名前に対して変更値を「=」で代入します。この列指定は複数指定が可能です(コンマ区切り)。さらに上のように足し算や引き算などもできます。続くWHERE句が無いと、StockTableのStockが全部+10されてしまいます。上では薬草のみを10個追加したいので、StockTable.ID=1001番にS限定しているわけです。

 このSQLをsqrt3_exec関数に渡せば、テーブルは新規追加もしくは更新されます。

 さて、ここでふと思うのが「UPDATEで新規追加できないの?」という事です。これは、残念ながらできません。テーブルに無いレコードを更新しようとすると何も変更されずに終わります。面倒なのが変更SQL文自体は正しいため、関数がエラーを返してくれないんです。レコードが無くてUPDATEされなかった事を検出するには、例えば次のようにします:

更新がだめなら新規追加
// データの修正
int CurItemNum = -1;
sprintf( str, update_stock_SQL, stockSize, ID );
sqlite3_exec( pDB, str, StockItemNumCallback, (void*)&CurItemNum, &errMsg );
if ( CurItemNum == -1 ) {
   // データの新規追加  
   sprintf( str, insert_stock_SQL, ID, stockSize );
   sqlite3_exec( pDB, str, 0, 0, &errMsg );
}

CurItemNumに-1という初期値を入れておきます。UPDATEのSQLを発行してみて、この数値が変わらなければ指定したStoclItemCallback関数が呼ばれなかった事になりますので、UPDATEする対象が無かった事になります。よってif文の中に突入して新規データ登録となります。

 ここをもう1度通った場合、すでにストックテーブルには薬草10個(ID=1001, Stock=10)が登録されていますので、UPDATEが有功となり、コールバック関数内に現在の登録数10+[追加分]が返ってきます。それをCurItemNumが受けますのでif文はスキップされます。

 こういう感じにSQLiteとSQL文があると、様々なデータ抽出やデータ管理を簡単に行うことができます。しかも追加や変更は直ちにデータベースファイルに反映されますので、データの永続性も完璧です。SQLiteはあまり巨大なデータベースを管理できませんが、ゲームのアイテム程度であれば全く問題ありませんので、皆さんのゲームで使用されてみては如何でしょうか。