AIプログラムとかUnityゲーム開発について

探索や学習などを活用したAI系ゲームを作りたいと思います。

Androidタブレットでアプリを動かす

Android向けにビルドしてみました。
やり方は、こちらを参考にしました。akira-watson.com

野良アプリ作るだけなら簡単ですね。
こちらからダウンロードできます。
http://rafeel.s602.xrea.com/quiz.apk
Androidのstoreに公開するには3000円ほど払って登録が必要ですが、
iPhoneのように年会費はいらないので安いですね。


うちのタブレットASUS製の7インチですが、CPUはIntelAtomなのでx86です。www.asus.com

バイナリ形式は、ARM+Atomの両方で行いました。
スマホはARMなのでこれならどっちでも動くのでしょう。

f:id:yasu9780:20150504115313j:plain

問題なく動きました!
uGUIには多解像度に対応する機能が色々とあるので
そのへんもいづれ調べてみたいですね。

クイズゲーム

今回は趣向を変えて、
クイズゲームを作ってみました。

http://rafeel.s602.xrea.com/webplay.html
WebPlayer版もビルドしたので実際に遊べます。
問題は2600問ぐらい入ってます。
難易度はかなり高いです。


f:id:yasu9780:20150504012457p:plain

uGUIの勉強で前から少しづついじってたのですが、
今遊んでいる某社のクイズゲームの攻略wikiにクイズがあったんで
そのデータをお借りしました(というか本番のための暗期練習に使えます)
せっかくunityなのでunity-chanもお借りしました。

uGUIの構造としては、Canvas→Panel→クイズの問題、選択ボタン
といった構造
パネルは、解答用とジャンル選択用があって、初めはジャンル選択用を表示させて、
パネルの表示・非表示でメニューを切り替えてます。


正解とか不正解なんかの文字。縁取りや影つけもできます。
文字を大きくするとややボケますね。
fontサイズで直接大きくしても20ぐらいから大きくならないので
Scaleで大きくしてるんだけど、なんか間違ってるのかな?
あと、漢字のfontがなんか変な気がする。なんか中国語みたいな日本語。


【Unity開発】uGUIのTextまとめ【ひよこエッセンス】 - ひよこのたまご Unity5(C#)でゲーム開発入門
こちらを読んで分かりましたが、テキストを表示するエリアを大きくしてないから
fontサイズを大きくすると表示されない模様。解決しました!



あと、音を初めて出してみました。正解音、不正解音
さすがゲームエンジンなのですごい簡単に出ました。


問題の読み込みです
問題はテキストファイルをResourcesフォルダに置いて、読み込んでます。"/"でjoinされたCSVファイルになります。

qq=new ArrayList();
TextAsset csv = Resources.Load("qow") as TextAsset;
StringReader reader = new StringReader(csv.text);
while(reader.Peek()>-1) {
	qq.Add ( reader.ReadLine() );
}


問題の構造は、

ジャンル番号/問題/答え/選択肢1/選択肢2/選択肢3

になります。すべて四択問題です。


まず問題全体から任意の問題を選び、ジャンルを表示します
(ジャンル限定ではこの時に特定ジャンルで縛ります)
次に選択した問題の答えと誤答選択肢を、
4つのボタンにランダムに配置します。

string qqq=(string)qq[no];
string[] values = qqq.Split('/');
GameObject.Find ("SectionText").GetComponent<Text>().text=Section[ int.Parse(values[0]) ];
string atxt = "";
string atxt1 = "";
string atxt2 = "";
string atxt3 = "";
int mode = Random.Range (0, 10);
if (mode == 0)
{
	atxt=values[2];GoodNo=1;
	atxt1=values[3];
	atxt2=values[4];
	atxt3=values[5];
}
…
quizText="問題: "+(string)values[1];
bt1.text = atxt;
bt2.text = atxt1;
bt3.text = atxt2;
bt4.text = atxt3;

あとはボタンをクリックしたタイミングで、答えとあっているか判定し
合っていれば、「正解」表示、正解音、unitychanがWINモーションといった感じです

迷路を歩くセーラー服さん

f:id:yasu9780:20150429224420p:plain

セーラー服さんを迷路の中を歩かせました。
基本、向きをスムースに変えて、アニメーションで歩行させると、進行方向に進むという仕組みですが、
回転の角度が小さすぎると隣のマスにいけずに回転するので、回転角度を増やしました。

迷路でのゴールをゾンビに設定して、距離を比較することになりますが、
ゾンビ20体だとけっこう処理が大変になるかも?
一度計算した結果は残しておくなどキャッシュする仕組みを入れればいいかもしれません。
迷路の壁がひとつなくなるなど、動的に環境が変わった場合は、
一度キャッシュを無効にして、再度計算し直すということで。

あと、マップが大きくなると再帰でのスタックオーバーフローの可能性も出てくると思いますが、
調べるとC#は標準はスタック1MBらしい。unityはmonoなのでわかりませんが。
.net - Stack size under Mono - Stack Overflow
こちらをみると、1レベルで100バイト消費とありますが、
今のところローカル変数も使ってないし、
1MBで1レベル100バイトなら、10000は潜れるので、まあ大丈夫かな?
ちょっと曖昧ですが。
知恵袋あたりだとA*のプログラムを作っててスタックオーバーフローに困ってる人はいましたが。
C#のスタック領域 【OKWave】

経路探索実装中(超面倒くさい)

map=new string[10];
map[0]="#####";
map[1]="#G..#";
map[2]="#.###";
map[3]="#...#";
map[4]="#.#.#";
map[5]="###.#";
map[6]="#...#";
map[7]="##.##";
map[8]="#S..#";
map[9]="#####";

#は壁 Gはゴール Sはスタート位置

Gからコストどれだけで移動できるかを再帰的に探索
SearchMap()で求めた表がこれ
f:id:yasu9780:20150428211047p:plain
(上下が逆なので注意)

いま、どこにいるかを指定すれば、そこからゴールまでの最短経路は、
8方向の値が小さいマスを選べばいい


スタートからゴールに移動中
f:id:yasu9780:20150428211359p:plain
はいゴール
f:id:yasu9780:20150428210511p:plain
ターン制RPGなら1マスつづ移動でもいいけど
そういうわけにもいかないので、スムーズな移動を実現

探索は超単純で
(map3は初期値はすべて-1が入っている)

void SearchMap(int x,int y,float cost)
{
	if(x<0) return;
	if(y<0) return;
	if(x>5) return;
	if(y>9) return;
	if(map2[x,y]==1) return;
	if(map3[x,y]==-1) map3[x,y]=cost;
	else
	if(map3[x,y]>cost) map3[x,y]=cost;
	else return;

	SearchMap(x-1,y,cost+1f);
	SearchMap(x+1,y,cost+1f);
	SearchMap(x,y-1,cost+1f);
	SearchMap(x,y+1,cost+1f);

	SearchMap(x-1,y-1,cost+1.4f);
	SearchMap(x-1,y+1,cost+1.4f);
	SearchMap(x+1,y-1,cost+1.4f);
	SearchMap(x+1,y+1,cost+1.4f);
}

すでにコストが入っている場合も、自分がより低いなら書き換える
壁や、コストが自分と同じ、または高いなら、戻る
斜めコストを1.4=√2として設定している

面倒くさいのが座標合わせ(´・ω・`)
もっとはじめにやっておくべきだった。
ある程度作ってしまってるのに、経路探索で距離を判断するように修正するのは
かなり面倒でバグまみれになりそうな予感。

墓からゾンビが這い上がってくる

f:id:yasu9780:20150427230503p:plain
 ランダムに配置ってわけにもいかないので、
 spawnPointを作りました。
 墓が多いとより分散した場所からゾンビがでてくるので各個撃破しやすいですが、たまたま墓が近いと集中してゾンビが出てくるのでやや難易度が高くなる。

void SpawnZombie()
{
	int no = Random.Range (0, MaxTombs);
	Vector3 v = tombObj [no].transform.position;
	v += Vector3.down * 1.5f;
	objZ [zno] = GameObject.Instantiate (Zom_Prefab, v, Quaternion.identity) as GameObject;
	float a = Random.Range (0, 360);
	objZ [zno].transform.Rotate(0 ,a,0);
	objZ [zno].GetComponent<ZombieController>().no=zno;
	zno++;
}

どの墓から出すかをランダムに選び、ゾンビの初期配置を墓の下に設定して、Instantiateする。


墓の素材をAssetStoreで検索したらFreeが全くないので、
(tomboとかgraveで検索)
Freeのfbx配布サイトからお借りしました。
西洋の墓ですが、tomboで画像検索すると遺跡的な墓で、
一般の墓はGraveかなと画像検索からは判断しました。

 出血を、セーラー服さんは赤い血で、ゾンビは緑の色にしました。
 あとステータスを一部日本語表記にしました。

 今日は細かい修正でしたが、今後は、プレイヤーは射撃の娘を操って、いかにセーラー服さんが戦士しないように援護して、ソンビを殲滅するのをゲーム性にしてみようと思います。
 間接的にAIを誘導できるとよりゲーム性が増えると思います。

  • クラッシュ・オブ・クランのゲーム性

 例えば、クラッシュ・オブ・クランだと、AIを「どこで」「いつ」投入するかにゲーム性があります。
 あっちのアーチャーは射程範囲のターゲットをロックオンして、無くなるまで打ち続けるという仕組みですが(矢を放つモーション中に敵が死ぬと空撃ちする)
 壁が破れた場合は、敗れたところを通過して、ターゲットに向かうという、経路探索も入っています。
 あと、バーバリアン・アーチャーは無差別ターゲット、ジャイアントは攻撃施設を狙うという、ターゲットの違いもゲーム性を生んでいる。

 初心者も簡単に操作できて、やり込むとより戦術や戦略を駆使できるという優れた仕様だと思います。

ゾンビからも攻撃

ゾンビからも攻撃をするようにしました。
画面上で膝を付いているのが殺られてしまったセーラー服さんです
(AssetStoreのSportyGirlのモーション利用)
時間が経つと回復します←ゾンビですかw

f:id:yasu9780:20150426214005p:plain

  • uGUIで右上に残りのゾンビ数などを表示
  • 画面が寂しいので、AssetStoreで無料で落としてきた木を実行時にランダムに配置
  • ステータス横の(数値/数値)は、(近くの敵数/近くの味方数)


f:id:yasu9780:20150426214620p:plain

ゾンビを全滅させるとステージの中央に整列して、全員で勝利ポーズをとります
平和は守られた



ターゲットにするゾンビの評価は、

float rate =(float)other.nearFriendCount/(float)(other.nearEnemyCount+1);
tmpPts = tmpDis;
if(other.nearEnemyCount>=3) tmpPts+=(float)other.nearEnemyCount;
if(other.nearEnemyCount<=1) tmpPts-=0.5f;
if(rate>=2f) tmpPts+=rate*0.5f; // too many
if(rate<=0.5f) tmpPts-=rate*1f;// pinch

既にセーラー服さんが3人以上近くにいるゾンビは低評価(過密なので)
逆に味方が1人・0人のゾンビを高評価

rateは、周囲のゾンビ数/周囲のセーラー服さん数で、
0に近いほど、セーラー服さん過密
other.nearEnemyCountが1以上で、数字が大きいほどセーラー服さんピンチ(囲まれてる)
other.nearEnemyCount==0の時は、単にゾンビが過密なだけで、銃撃の場合はカモ

うーん、上のコード間違ってますね(爆)
rateの評価が逆です(汗)

if(rate<0.5f) tmpPts+=1.5f; // too much sailor
if(other.nearEnemyCount>=1 && rate>=1.5f) tmpPts-=rate*1f;// pinch

こうすれば良さそう


いっそのことゾンビの頭に評価値を表示すればデバッグしやすい

f:id:yasu9780:20150426222611p:plain
ちょっとシュールな絵になった。
殺られたセーラー服さんは回復させない方が緊張感があっていいかもしれない。
むしろゾンビガールになって襲ってくるべき
緑色のテクスチャを作らねば

近くにいる敵の数を評価する

これまでターゲットを決めるには、一番近い敵をという距離を評価していましたが、各位が近くの敵の数を調べておいて、それをターゲットの決定評価に加えてみました。

tmpDis = Vector3.Distance (t1, t2);
tmpPts = tmpDis*(float)(other.nearEnemyCount+1)/3f;

tmpDisは距離で、tmpPtsは距離に近くの敵数を乗じたもの

  • 敵数0 : 距離*1/3
  • 敵数1 : 距離*2/3
  • 敵数2 : 距離*3/3
  • 敵数3 : 距離*4/3

周囲に敵数0のフリーな敵をより狙い、周囲に敵が多い(=味方が多い)敵は低い評価にする。
ただ乗算だと副作用も大きいので、距離+近くの敵が3人以上は減点
みたいな和の評価の方が安定するかも。

f:id:yasu9780:20150426104338p:plain

この図で、ステータスの横の(2)といった数字が、近くの敵数です。
左側にいるゾンビは近くに敵が2人いると認識していて(左右から01と02に挟まれている)
すでに味方が囲んでいる敵を低評価することで、
右端の02が、より近い左のゾンビではなく、上方の遠いゾンビに向かうようにするのが狙い。
この評価法で、特定の敵に固まらずに、ばらけて各個撃破するようになるはず。

ただ、単独攻撃が増えるので、少なくともペアで当たるほうが安全という評価は
別個入れる必要があると思います。

あと、遠距離攻撃者はまた話が変わってきて、
基本的に、味方が狙っていない離れた敵を優先して攻撃したほうがいい。
逃避中ゾンビだと放置すると逃すし、近接攻撃者が追うとコストが高すぎる
ただし、脅威を考えた場合、一番近いゾンビからは回避しないといけないわけで。
(このへんは将棋も同じで、離れ駒を金銀で追うと移動コストが丸損で、飛角で狙うべき。もっとも飛角でさえも手損は発生するが、将棋はターン制だから)

  • 攻撃対象は遠くて味方が狙っていないゾンビ
  • 退避対象は近いゾンビ(味方が周囲にいないとさらに危ない)

ということで、遠い敵と近い敵という二重に評価する必要がある
なかなか複雑(^_^;)
要は攻防の1手で行動すべし。