スマホゲームとAI
スマホゲームの表現の幅を広げるのにゲームAIの技術を使うことは非常に有効だと思っています。
・なぜAI
コンソールゲームとスマホゲームの決定的な違い(ビジネスモデルを除く)はやっぱり操作系のインターフェースじゃないでしょうか?
今のプレステだと十字キーやアクションボタンはもちろん、ジョイスティックやLRボタンなど操作すべきボタンが沢山あります。しかしスマホはタッチパネルのみで、電車の中でつり革につかまったりすることを想定すると片手で操作されることになります。そうするとプレイヤーからの入力がコンソールゲームに比べて著しく少なくなります。その結果プレイヤーの動きを少ない入力で表現できるゲームに特化されることになり、ゲームの方向性が決まってきてしまう気がします。
じゃあ、コンソールゲームの時に必要だった入力をAIが補うようにすればどうでしょう?どの操作を実際にプレイヤーがやって、残りの操作をAIに任せるかという選択ができるようになります。それにより操作系には過度に縛られないゲーム作りができるような気がします。ゲームAIは元々は敵キャラを動かすのに使うのが通常ですが、自分が操作するキャラにも使ってしまうわけです。
で、実際勉強がてら作ってみました。kondroid.hatenablog.com
マップを移動して、武器を選択して敵を攻撃するというありふれたアクションゲームですが、プレイヤーが操作するのは移動地点を選択するだけでその他はすべて自動です。おそらく、方向キーを使って操作してボタンで武器を選択してボタンで攻撃するという操作をいちいちプレイヤーがやっていたらスマホゲームとしては成り立たないでしょう。しかも操作する味方が5体もいるわけですし(笑)。
・問題点
ただ、作るにあたっての問題点ももちろんありました。やっぱりCPUパワーを結構使うことになります。バッテリーという制約があるスマホではここの対策は必須かと。ゲームAIの本に必ず出てくるA*アルゴリズムは処理が重くて使えませんでした。代わりにノードの最短経路をあらかじめテーブルにしてそれを参照する方式(経路テーブル)を採用しております。この方式はマップの経路が途中で塞がれたりという動的に変化するマップでは使えません。しかし前述のA*アルゴリズムと比べると圧倒的に高速です。他にもゲームAIはあんまり関係ないですが衝突判定の効率化などCPUをなるべく使わない工夫を入れています。まだまだ改良の余地はありますが。
・作ってみて
間違いなくゲームの表現の幅は広がりました。ゲームAIの基本中の基本の部分しか取り入れてませんが、それでもかなり強力だと思います。個人的な意見ですが、スマホゲームではプレイヤーのガチの能力だけじゃなくて、運の要素もゲームを面白くさせる大きな一因となっている気がします。であれば、多少はバカなAIでもいいのかななんて思ったりもします。いずれにせよ、ゲームを面白くするのが目的なので、使えるところはどんどん使っていきたいと思います。
新作アプリ「イカタコ戦争」をリリースしました!!
新作のゲームアプリをリリースいたしました!
※起動時の爆音にご注意ください。すみません。
イカタコ戦争
遊び方
ゲーム画面
タコを操って因縁のライバル!?のイカを倒しましょう。
操作は簡単、画面をタップしてタコの位置を移動させるだけです。タップした位置に自動で移動してくれます。攻撃もすべて自動でやってくれます。
・まずは武器の選択から
武器には通常武器と特殊武器の2種類があります。プレーヤーは通常武器と特殊武器の二つを使って敵と戦います。
1.通常武器
通常武器は一種類です。
タコすみ
小さい墨を発射します。威力は弱いですが弾数は無限です。
2.特殊武器
特殊武器は三種類あります。弾数に制限があります。
バブル弾
泡を発射します。弾けてダメージを与えるので当たり判定が広いです。
タコすみブラスター
散弾銃のように墨を発射します。敵との距離が近いほどダメージは大きくなります。
触手
触手を敵にぶつけます。非常に速度が速く、運が良ければ倍のダメージを与えることができます。
武器選択ボタンで特殊武器を選びましょう。
・操作方法
画面をタップするだけで勝手に動いてくれます。壁の回避もすべて自動です。
1.まずは移動させたいタコを選択しましょう。タコを選択するには画面中のタコをタップします。タップするとタコの周りが緑のフレームで囲まれます。
もう一つ、タコを選択する方法があります。下のステータスボタンをタップします。タップすると上と同じようにタコが選択されます。
2.上記のようにタコを選択した状況で、移動させたい地点をタップします。すると黄色いピンが出てきてそこに移動します。途中で壁がある場合は最短経路をたどるようにまわり込みます。
3.移動終了後は各自また勝手に動き出します。アイテムを取りに行ったり、敵を攻撃したり、さまよったりします。
・攻撃
攻撃はすべて自動で行います。通常武器と特殊武器の切り替えも状況に応じて自動で行ってくれます。特殊武器の残弾が0になると通常武器による攻撃に切り替わります。
・HP
敵味方ともにHPがあり、0になると戦闘不能になります。キャラクターの上にあるバーやステータスボタンのハートマークが残りHPを表します。
・アイテム
マップ上にはアイテムが落ちてあってそれを拾うと特殊武器の弾数が増えたり、HPが回復したりします。敵味方共通なので、敵にアイテムを取らせないことも戦略の一つです。
1.HP回復
HPが回復します。
2.特殊武器の弾
特殊武器の残弾が増えます。
・勝敗
敵を全滅させれば勝利ですが、全滅させられれば負けです。いずれにせよ経験値は手に入りますが、負けの場合は獲得経験値は半分になります。
・経験値
獲得できる経験値は敵に与えたダメージの大きさに比例します。もし敵に一度もダメージを与えられなかった場合は経験値は0になります。育てたいキャラは優先的に操作して敵にダメージを与えに行きましょう。
・レベルアップについて
経験値を稼ぐと味方のレベルがアップして強くなります(最大でレベル100)。一方、敵に勝利すると敵のレベルが一段上がって強くなります(最大でレベル30)。
説明は以上です。
急いで仕上げたのでかなり作りが荒いですが、そこそこは楽しめるものになったんじゃないかと思います。
現状だとまだまだギミックも全く足りないし、おバカな動きをしたりと改善点は多々ありますが、それはおいおいやっていこうかと思います。
あとAndroid版も。
面白そうだなと思われた方はぜひ遊んでみてください!
Cocos2d-xでゲームを作るときのタチの悪いメモリバグを追跡する
個人開発者の方はこれで結構救われるんじゃないかな?
Cocos2d-xを使うゲームはC++で記述するので、メモリ関連のバグに悩まされるケースが多いと思います。Xcodeでデバッグしてその場所を突き止められるのならいいのですが、メモリのオーバーランなんかだとしばらく問題なく動いた後に突然訳のわからないところでエラーが出てアプリが落ちるということになってしまいます。こうなると原因を特定することは非常に困難です。コンソールゲームの開発であればグローバルなnewを拡張するなど独自のメモリ管理の仕組みを使ってこれを検出するのが通常のようですが、少々難易度が高いです。自分もどうするか悩んでいたところ、stackoverflowにこんな記事を見つけました。
stackoverflow.com
注目は二つ目の回答。なんとXcodeでそれを検出することができるのだそうです。Edit SchemeのDiagnostics内のEnable Guard Mallocにチェックを入れます。そしてデバッグするのですが注意点があって、実機はダメでシミュレーターのみでこの機能が使えるそうです。Appleの公式もこちらにあります。developer.apple.com
で早速使ってみたんですが、いや〜もう出るわ出るわ(笑)。えっ、マジかよってな間違いがでてきて凹みます。
iOSやAndroidは仮想メモリシステムが入ってる分、ヒープの断片化はあんまり問題にならないのですが、こういうちょっとして死ぬ系のバグは起こります。それを見つけるためにこのXcodeの機能は非常に有効ですね。
お悩みの方はぜひ一度お試しあれ。
(追記 4/23)
これはメモリのバグを解決するものではなくてあくまで検出するためのものです。また、すべてのケースで検出できるかどうかは不明です。メモリリークやそもそものメモリ不足(特にiOS)などその他メモリ周りに起因するバグも存在します。メモリ管理には引き続き慎重な姿勢が求められることに変わりはありません。
新作アプリ作りました↓↓kondroid.hatenablog.com
シードフィルアルゴリズムでナビグラフのデータをつくる(その3)
前回の続きです。kondroid.hatenablog.com
ようやくソースコードを見ていきます。早速始めましょう。
ソースコードはこちら。
https://github.com/kondroid00/MapEditorSample
いきなりですが核心部分です。
void MapGraphGenerator::createGraph(const Vec2 &seed, float nodeDistance) { //ノード間の距離を指定 m_NodeDistance = nodeDistance; //セルの作成してシードのセルを返す auto pSeedCell = createMapCells(seed); //シードセルに探索済みチェックを入れる pSeedCell->check(); //Wall上にあるセル全てに探索済みチェックを入れる(無効化) checkCellOnWalls(); //シードフィルアルゴリズムでセルを有効化する calculate(pSeedCell); //有効なセルに番号を打つ setMapCellsNum(); //ノードとエッジを作成 createNodeAndEdge(); }
MapGraphGeneratorというクラスがあり、その中のこのcreateGraphという関数が今回の肝です。引数にはシードの位置とノード間の距離を指定しています。m_NodeDistanceでMapGeneratorはノード間の距離をメンバ変数に持ちます。MES_Mapというクラスがあり(MESはプロジェクト名MapEditorSampleの略)、こいつのインスタンスがあらかじめ壁の情報をResources/Map/wall.mapから読み込ん(MES_Map::loadWallFile)でメンバ変数として持っており、MapGraphGeneratorはそれをコンストラクタで受け取って参照を持っておくことにしています。ちなみにMES_Mapはノードとエッジもメンバ変数(コンテナ)として持っており、それらもMapGraphGeneratorが参照を保持しています。そしてノードとエッジができたらMapGraphGeneratorが直接参照を使って情報を書き込むようにしています。とりあえずソースコードはこんな感じ。
class MES_Map { private: std::vector<Wall*> m_Walls; std::vector<NavGraphNode<> > m_Nodes; std::vector<NavGraphEdge> m_Edges; .... public: .... void loadWallFile(const std::string &wallFullPath, float offsetX, float offsetY); const std::vector<Wall*>& getWalls()const{return m_Walls;} std::vector<NavGraphNode<> >& getNodes(){return m_Nodes;} std::vector<NavGraphEdge>& getEdges(){return m_Edges;} .... }; class MapGraphGenerator { .... private: const std::vector<Wall*> &m_Walls; std::vector<NavGraphNode<> > &m_Nodes; std::vector<NavGraphEdge> &m_Edges; float m_NodeDistance; .... public: MapGraphGenerator(const std::vector<Wall*> &walls, std::vector<NavGraphNode<> > &nodes, std::vector<NavGraphEdge> &edges) :m_Walls(walls) ,m_Nodes(nodes) ,m_Edges(edges) {} .... }
その2の③で画面いっぱいにノードが作られたと思うんですが、あれは最終形態ではなく一時的なノードです。最終形態は⑦で見たようなノードです。ここでこの一時的なノードをMapCellという構造体で表します。MapGraphGeneratorの内部ではこのMapCellを使って計算を行います。流れとしては
- MES_Mapが壁の参照を渡し、ついでに空のノードとエッジの参照を渡す。
- MapGraphGeneratorがMapCellを使って内部で計算をし、ノードとエッジにデータを入れる。
- MES_Mapのノードとエッジにデータが反映される。
という流れです。ではMapCellを見てみましょう。
struct MapCell { int num; cocos2d::Vec2 position; int x, y; //探索済みならtrue bool checked; //有効なセルならtrue bool affective; public: MapCell(const cocos2d::Vec2 &position, int x, int y) :num(0) ,position(position) ,x(x) ,y(y) ,checked(false) ,affective(false) {} .... };
MapCellのnumはただの通し番号です。ただ、その2の⑤の状態になってから初めて番号が与えられます。positionは位置です。xとyは左下をとした際の行と列の番号です。例えばその2の③のシード(赤い点)だとx=3、y=3になります。checkedはその2の④の過程で使います。④の一番上の赤い点は上下左右を探索しすべてOKだとわかりました。その後、今度は赤い点の右にあるMapCellが上下左右の探索を始めます。この時左側の赤い点はすでにOKだとわかりきっています。ゆえに調べることはしません。すでに探索済みのMapCellはcheckedをtrueにします。最後affectiveです。これはそのままMapCellが有効ならtrueにします。その2の④では一番上の4つの黒い点はtrueですが、下の二つの黒い点はfalseです。
..........うーん、長い。
ですから、ここからは巻きでいきます。これ以降についてはソースコード内にコメントを書いておきましたのでそちらを読んでみてください。MapGraphGenerator::createGraph内の以下の4つの関数
- createMapCells
- checkCellOnWalls
- caluculate
- createNodeAndEdge
について軽く説明しておわりにします。
1. createMapCells
その2の③のぽつぽつを作ります。ぽつぽつはMapCellのオブジェクトになってMapGraphGeneratorのメンバ変数m_MapCellsにポインタが格納されます。戻り値はシードのMapCellオブジェクトのポインタです。引数が位置だったのでオブジェクトのポインタになって戻ってきたってことですね。
2.checkCellOnWalls
これが必要だと気づくのに時間がかかったんですよ。壁の上にあるMapCellどもをaffective=falseのままチェック済み(checked=true)にしちゃいます。これがないとその2の⑤で壁を通り抜けちゃいます。おそらく次のcaluculateの実装にもよると思うんですが、とにかく今回は必要なんです。で、問題はどうやって線分で表現される壁のうえに点で表現されるMapCellが乗っかっちゃってるかを調べるかなんですが、線分と点の距離を求めて、その距離が結構小さい(今回は1e-12)ければ乗っちゃってるってことにしてます。floatを使う以上誤差が出ちゃうから決して==で判定してはいけません。ソースみてね。(追記)floatなのでこれじゃ精度がまずいですね。他にも色々問題があるので少し考えます。
3.caluculate
シードフィルアルゴリズムそのものの実装で今回の目玉です。まず引数は起点となるMapCellのポインタです。って言われてもピンとこないでしょうから、その2の③のシード(赤い点)を引数に入れてみましょう。その2の③ではシードの上下左右は間に壁もなく近くに壁もないので4つともすべて有効(MapCell : affective == true)です。有効だったらどうするか。今度はその4つのMapCellを起点にして同じことを繰り返せばいいのです。そこでこの関数のしょっぱなにcontainerを用意してその中にこの4つのMapCellのポインタを打ち込みます。そしてこの関数の最後でcontainerから値を取り出して、そいつを起点にまた自分自身caluculateを呼び出しています。いわゆる再帰呼び出しというものです。ここで呼び出されたcaluculateはまた内部でcaluculateを呼び出し、またそのなかでcaluculateを呼び出しと延々と続いていきます。じゃあいつこの関数は止まるのか。containerの中身がなくなった時すなわち、すべてのMapCellが探索済み(checked == true)になった時です。このcheckedがなければ無限ループになると思います(試してないけど...)。この関数が終了すればMapCellのaffectiveがtrueとfalseのふた組のMapCellどもに分かれ、affective == trueのMapCellどもがその2の⑤のポツポツがになります。
4.createNodeAndEdge
これでラストです。ハァハァ。片っ端からMapCellを調べて行って有効(affective == true)なら処理をします。どういう処理かって?その2の⑥をごらんください。サンプルコードはネストがひどくて見る気が失せそうですが、その2の⑥をやってることは確かです。さらにこの関数の内部でノードとエッジを作成し、MapGraphGeneratorのメンバ変数(コンテナ)m_Nodesとm_Edgesにそれぞれ格納しています。二つはMES_Mapのノードとエッジのコンテナの参照なので、この関数を抜ければMES_Mapはノードとエッジの完成品を持ってることになります。
これで解説は終わりです。素直に疲れました。
ついでにアニメーションの方も軽く解説しておきます。まずアニメーション用なのでMES_Mapにノードとエッジを作って返していません(笑)。caluculateのところがcreateNodeCycleOnceになってupdateで呼ばれています。次に探索するMapCellをMapGraphGeneratorのメンバ変数のキュー(m_CellsQueue)に格納し、そこから起点を取り出す仕組みです。また有効化されたMapCellはMapGraphGeneratorのメンバ変数のコンテナ(m_AffectiveCells)に格納されます。m_AffectiveCellsはまずシードから格納されていきます。MapCellも自分の八方向にあるMapCellのポインタを保持しています(m_CellList)。これはエッジを描く時に必要です。次にcreateEdgeCycleOnceでエッジを描いていきます。これは簡単で、m_AffectiveCellsから一つづつMapCellを取り出し、そのMapCellが保持しているm_CellListから取り出したMapCellとでその2の⑥を調べ、OKならエッジを描画するだけです。m_AffectiveCellsはシードから格納されているのでシードからエッジが広がるように見せることができます。
さらに疲れました。とりあえずソースコード見てみてください。
以上です。
新作アプリ作りました↓↓kondroid.hatenablog.com
シードフィルアルゴリズムでナビグラフのデータをつくる(その2)
前回の続きです。
前回はアプリでシードフィルアルゴリズムの動きを見ていきましたが、今回はソースコードに触れていきます。ただし、今回はアプリでみたプログラムのソースコードには触れません!えっ、なんでと思うかもしれませんが、アプリのプログラムはナビグラフが出来上がっていく様子をアニメーションで見せるための実装になっています。最初はシードを設定したらすぐにバンってナビグラフが出来上がったものが出てくるようにしてたのですが、面白くないのでアニメーションっぽくしてみました。実用レベルではわざわざアニメーションにする理由はないですし、アニメーションにしないほうがわかりやすいので、今回はシードを設定したらいきなりバンってナビグラフが出来上がったものが出てくるやつを見ていきます。
とりあえず大雑把に理解する
いきなりですが図解です。
これで終わりです。アプリの動きをみると一見難しそうですが、やってることは極めて単純です。
①シードの設置
まずはシードを設置します。このシードの位置がおかしいと変なことになります。例えばアプリで壁の外をタップしてみると、壁の外にナビグラフが出来上がります。また、ノードは壁からある一定距離のところじゃないと生成されないのですが、シードは壁から近くても置けてしまいます。そうすると太った人がこのシードにやってくるとお腹が壁にあたってしまいますね。正しい位置にシードを設置する必要があります。
②画面の分割
次に指定したノードの間隔で画面を縦横分割します。画面の縦横の分割線の交点にシードがあるようにします。これはシードを基準として上下左右方向にノード間の距離を取っていくことで対処できます。
③画面をノードで埋め尽くす
②でできた縦横の分割線の交点上にノードを作成します。
④あるノードを基準として上下左右のノードをチェック
まずは③でシードの上下左右を見てみましょう。シードと上下左右のノードの間には壁はありません。シードの上下左右のノードは有効です。次にシードの一つ下のノードを見てみましょう。こちらは上下と右のノードは問題ありませんが、左のノードは壁との距離が近すぎます。このノードに太った人がやってくるとお腹が壁につっかえてしまうでしょう。ゆえにノードとしては有効ではありません。
⑤有効なノードだけを残す
④で得られた有効なノードだけを残します。シードの周りのノードは上下左右が有効でした。そのあとはどうすればいいでしょうか。答えは、シードの上下左右のノードを基準として同じことを繰り返せばいいのです。
⑥エッジを作成する
④と同じような図ですが考え方も同じです。基準とするノードとその8方向のノードの間間のノードを調べ、各々のノード間で何もないかつ壁と近すぎなければエッジを作成し、間に壁がある、または壁と近すぎる場合はエッジを作成しません。壁と近すぎるのが問題の理由は④と同じで太った人が通るとお腹がつっかえてしまうからですね。
⑦すべてのノードでエッジを作成する
これは有効なノードをどの順番でもいいので片っ端から調べていけばいいです。すべて調べ終われば完成です。
以上が大雑把な理解です。少し細かいですが、一つ一つを追っていけばそんなに難しくはないと思います。ソースコードにも触れる予定でしたが、長くなってしまったのでソースコードは次回に持ち越します。
追記
その3を書きました。
新作アプリ作りました↓↓
シードフィルアルゴリズムでナビグラフのデータをつくる(その1)
- 作者: Mat Buckland,松田晃一
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/09/28
- メディア: 大型本
- 購入: 8人 クリック: 262回
- この商品を含むブログ (43件) を見る
この本、すごくおすすめです。
私はCocos2d-xでゲームアプリを作っています。Cocos2d-xというのは描画やサウンドといったゲーム開発に必須な要素を担うフレームワークですが、肝心のゲームロジックの部分は自分でちゃんと書かなきゃいけません。そのゲームロジックに関するプログラミングのヒントがぎゅっと濃縮されています。
私もこの本で学習をしていたのですが、経路探索に使うナビグラフの情報が入ったマップファイルの作成は本書の中では.exeファイルで提供されていて、ソースコードはありませんでした。ですので今回はそのマップファイルを作るプログラムをつくってみました。
ソースコードはこちらです。ソースコードの一部は上の本から拝借しております。
https://github.com/kondroid00/MapEditorSample
(cocos2d-x v3.4でiPhone6(実機)でのみ動作確認をしております。)
シードフィルアルゴリズムというのを使います。ペイントソフトで色塗りをする際によく使われているそうです。とりあえずプログラムの動作を見てみましょう。
ノード=点、エッジ=ノードとノードを結ぶ線と思ってください。
起動するとRPGの城の中のようなマップが現れます。
画面をタップするとそこをシード(種)として周囲に一定間隔でノードができていきます。その際に新しい点が壁を突き抜けることはありません。
ノードが作成されなくなったらこんどはノードを中心にして周りにエッジができていきます。すべてできればプログラムの動作は完了です。
画面を下にスワイプすると下のような中に数字が入った円のようなものが現れます。(逆に上にスワイプすると隠れます。)
円の中をタッチしたままにすると円が赤くなります。その状態で時計回りになぞると数字が大きくなり、反時計回りにすると数字が小さくなります。実はこの数字はノード間の距離を表しています。円の少し上にある骨みたいなのが実際の距離です。
他に、画面を長押しすると画面に描かれたすべてのノードとエッジが消えます。
上のアプリを使うとシードフィルアルゴリズムの動きと生成されたナビグラフがどんなものかがなんとなくつかめますでしょうか?で、このナビグラフ一体なにに使うんでしょうか?主にはキャラクターを壁に衝突させずにある地点からある地点に移動させるのに使います。しかも最短で。エッジを辿っていけばまず壁にはぶつからないですし、どれくらい移動しなきゃいけないかもすぐにわかりますよね。
次回はプログラムの中身に入っていきます。
追記
その2を書きました。
新作アプリ作りました↓↓
自作アプリのご紹介
個人でゲームアプリをつくってリリースしております。
NEW!!
イカタコ戦争
タコとイカが武器で撃ち合うというゲームです。
詳細はこちら↓↓
たまご番
iTunes の App Store で配信中の iPhone、iPod touch、iPad 用 たまご番
たまご番 - Google Play の Android アプリ
にわとりがやってきて卵を産む前に、前にやってきたにわとりが産んだ卵を回収するというゲームです。イメージ的にはワニワニパニックに近いです。正しく遊ぶためには端末を机の上に置いて両手の人差し指と親指をフル回転で使う必要があります。
ぬこキャンパス
iTunes の App Store で配信中の iPhone、iPod touch、iPad 用 ぬこキャンバス
おえかきアプリですが、絵を描いてる最中は全く描いた線が見えないという鬼畜おえかきアプリです。キャンパス内ではぬこがたくさんうじゃめいていてホントに邪魔です。これでジ◯ニャンを書けばダークサイドジ◯ニャンと友達になれます。