2014年9月28日日曜日

libgdxでスマホ用のゲームを作っての感想

今年に入ってからの仕事で調べた物を社内のWikiにまとめるという作業がとても多いのでこちらのブログに書くモチベーションがまったくあがらず放置しまくっていたけれど久しぶりに仕事とは離れたネタができたので更新。

前々から何らかのゲームを作りたいなとAndroid用にAndEngineをいじくってみたりHtml5用にEaselJSで遊んでみたりしてみたけどどうにもしっくりこない。そんなこんなで月日は流れたけれど今年の夏に一念発起して二ヶ月に一本ぐらいのペースでゲームをリリースしていこうと決めた。

で、八月の頭からゲーム開発を余暇を使ってしてきたんだけど先ほどつつがなく一本目のリリース(2014/11/06につつがなくApp Storeへのリリースも完了)が終わったので宣伝やら何やらも兼ねて楽しかったり大変だったりしたことをまとめたのが以下。今回は主に開発環境周りで。

・作ったゲーム
宣伝です。

Get it on Google Play



余談:「バレーボール ゲーム」で調べたらあんまり出てこなかったので油断して「Slime Volleyball」って名前にして作ってたらまんま同名の10万回ぐらいダウンロードされてるタイトルがあって内容も似てたのでとりあえず「Super Volleyball」にしてお茶を濁す。


・フレームワーク
libgdx
AndEngineとかEaselJSをいじってみて思ったんだけど同じ製品をAndroid用、Html用、iOS用とか作り分けるのはやっぱりめんどい。他プラットフォーム用に製品を作るのはロジック部分とかほぼほぼ転用できるので開発は思いのほか楽なんだけどそれでもメンテナンスを考えるとめちゃめんどい。リソースが自分ひとりで開発からグラフィック、リリースとかまで全部やるのにこれはかなりの重労働になるのでうまいこと他プラットフォーム用のコードを生成してくれるフレームワークがないかなぁと思っていて見つけたのがこれ。

Unityも考えたけどlibgdxはフリーだし必要そうな機能も満たしてたのでこちらへ。javaで開発できるのもいいね。C#でもできるっぽいけどC++とか今更触りたくないよ。

・libgdxの感想
さくさく開発できた。libgdxはビルドをGradleに任せてるんだけどそのお陰か私のメインIDEであるIntelliJ Ideaでのセットアップもすぐに完了し即コーディングに入れたのはベリーグッド。グッジョブだ。Box2dとかも初期設定で参照するようにできるし簡単だった。ただねGradleのことが良く分かってなかった(今現在もポワポワしてる)ので何らかのサードパーティのライブラリを追加するのは結構骨が折れたしこれからも骨が折れそう。

・コーディング周りの感想
scene2dという形でScreen、Actor、Actionとか画面の要素をうまいことまとめて使えるようにしてくれてるけどまだ自分の中でしっくりきてないしまだまだこなれてない。それなので模索しながら開発しつつ今現在もまだモヤモヤしたまま。けれど可能性は十分あると感じた。やっぱController的なレイヤーをしっかりと作ってView的な要素ともっと明確にわけたほうがスコアとか画面のステートとかがごちゃごちゃせずにすっきりするだろうなという感じ。

・フォント
これは今もってどうしたほうが良いのか謎。今回みたいにほとんどテキスト表示するものが無いものはあんまりこだわる必要もなくデフォルトのを使えばよいかなぁと思った。ちょっと凝った部分は画像を文字として使うことになるし。多言語化とか考えたら文字を画像にするのとかご法度だろうけど。

・ツール類
Texture Packerの存在を途中まで認識しておらず最初のころは自前で複数の画像を一枚の画像につめてその画像内の座標を指定して表示用に抜き取ったりという狂気の沙汰を続けていたのだけれどTexture Packerさんにご足労いただいてからはその自死一歩手前から引き返すことができた。とても便利。

さらにPhysics Body Editorなるめちゃんこ優秀なツールもあるのでBox2dを使う方は目を通されるとよいかと。

あとは2D Particle Editorとかもある。未使用なのでまだいまいち把握していないけれどエフェクトを派手にするためにも次のゲームでは使ってみようと考えている。

・マネタイズ
libgdxを選んだ理由にAdMobとかとの統合のチュートリアルがちゃんとあるというのも大きなウェートを占める。少し苦労したけれど無事にAdMobを実装し広告が出せるようになった。いずれはiOS版も出そうとは考えているけれどそっちはまだあまり調べていないので少し謎だけど一応問題なくできそう。

・グラフィック
Ink Scapeを使用。グラフィックの勉強をちゃんとしたこともないしPhotoShopで適当に画像をいじったりとかしかしたことなかったけれどInk Scapeは優秀なのでネットに転がってるチュートリアルを参考にいくつかサンプルを作ったりしてたら大分慣れてきてほぼほぼ満足いくキャラとか背景とかが作れた。まだ隠された機能がたくさんあるようなので精進あるのみ。非常に使いやすいのでお勧め。フリーだし。

・サウンド
なし。スマホゲームでサウンドはいらんかなと思ったのでばっさりと。サウンドやらBGMの作り方とかまったくの門外漢なので。今後も予定無し。

・iOS
Androidアプリは今までもいくつか作ったことがあったので勝手が分かってたけどiOSのほうは全然分からないので現在勉強中。アイコンとかでさえどっから手をつければよいのかがまったく分からん。iAdを組み込むならなおさら。
下記に追記。


まとめ
専門学校以来10数年ぶりのゲーム作成で面白かった。libgdxはさくさく作れるので良い。動作テストもDesktopのプロジェクトを含めておけばPCですぐに確認できるし。これがAndroidのEmulatorだけだったら遅すぎて発狂すると思う。



追記 2014/11/07
App Reviewが昨日承認されて晴れてApp Storeにリリースできた。iOSアプリを作ったことがなかったのに加えてlibgdxでiOS版をリリースするチュートリアルがEclipseメインで書かれているものが多くIntellij ideaを使ってのリリースはかなり大変だった。またAppleのチュートリアルもXCodeを使って開発することを想定してのものなので、libgdx+intelij ideaというiOSアプリ開発環境的に邪道感満載の環境で手動設定するべき値がどれなのかを調べるのには骨が折れた。iAdのほうがマネタイズ的にAdMobよりも断然おいしいという比較記事を読んでいたのでiAdを組み込みたかったのだけれどそこにたどり着くまでにかなり試行錯誤を繰り返しすぎて発狂しそうだったのでそこはすっぱりとあきらめてrobovm-ios-bindings/admobでさくりと実装した。それとApp Reviewはなんやかんやとやっぱり時間を食う。それでもSubmitしてから一週間ということを考えれば早いほうなのかな、と。

いずれAdMobの組み込み方とか、iOS版のリリース方法とかまとめたいと考えているけれどとりあえず年内にもう一本簡単なゲームをリリースしたいのでそれが終わってから気が向いたら書くかも。

2014年4月10日木曜日

html5のcanvasにレーダーチャートを描画する

レーダーチャートの話が仕事中に出て自前で作るかどこかのコンポーネントを購入するかという話になり結局購入することになったのだけれども、デモ用にちゃちゃっとレーダーチャートを実装したのがあるので公開しておく。

デモはこちら

実装周り
// Knockoutjs周りは省略

var canvas = document.getElementById("canvas")
 , c = canvas.getContext("2d");
function drawRader(){
 var points = viewModel.points()
    , eachRad = (Math.PI*2) / points.length
 , i
 , accumRad = Math.PI/2
 , radius = 150
 , center = 200
 , sin
 , cos
    , endOfAxisPt
 , firstPt
 , prevPt
 , currentPt;
 c.clearRect(0,0,400,400);
 for(i = 0; i < points.length; i ++){
  c.strokeStyle = "#ffa500";
  c.beginPath();
  sin = Math.sin(accumRad);
  cos = Math.cos(accumRad);
        endOfAxisPt = {
            x: center + (cos * radius),  
            y: center - (sin * radius)
        };
  accumRad += eachRad;
  c.moveTo(center, center);
  c.lineTo(endOfAxisPt.x, endOfAxisPt.y);
  c.stroke();
        
        c.fillStyle = "#00A0E9";
        c.fillText(points[i].text(),
                   endOfAxisPt.x - 10, 
                   endOfAxisPt.y - (sin * 20));
  
  c.strokeStyle = "#00A0E9";
  c.beginPath();
  currentPt = { 
    x: center + (cos * (radius * points[i].ratio())), 
    y: center - (sin * (radius * points[i].ratio())) 
   };
  if(prevPt){   
   c.moveTo(prevPt.x, prevPt.y);
   c.lineTo(currentPt.x, currentPt.y);
  }
  else
   firstPt = currentPt;
  prevPt = currentPt;  
  c.stroke();
 } 
  
 c.beginPath();
 c.moveTo(prevPt.x, prevPt.y);
 c.lineTo(firstPt.x, firstPt.y);
 c.stroke();
}
drawRader();
drawRader()の冒頭でviewModel.points()と取得しているのはKnockoutjs用のViewModelが内部的に保持しているObservableArrayだ(Knockoutjsを知らない人は配列のようなものと考えてもらって構わない)。描画の処理は単純で配列のアイテム数で360度を割ってその角度ごとに軸線を描画し、前後の軸線上の点と点を結んでいくだけ。

2014年1月4日土曜日

html5のcanvasに描画した曲線をヒットテストする

昨年はWPFを使ってGUIをグリグリ動かすアプリ開発にどっぷりはまっていたので今回は久しぶりにJavaScriptをいじりたくなり表題のものをガリガリ作った。

デモはこちら
githubはこちら(デモと差は皆無)

赤い四角が始点と終点だ。オレンジの四角が制御点になる。すべてdraggableなので任意に動かしてもらいたい。線上の点は曲線を直線に分割している点だ。曲線をクリックすると緑色になる。

曲線描画 on canvas
html5のcanvas上で曲線を描画するのは至極簡単だ。context.bezierCurveToまたはcontext.quadraticCurveToを呼び出せば良い。前者は制御点が2つのcubic bezierで後者は名称そのままの制御点1つのquadratic bezierだ。今回のデモではcubic bezierのほうを使用している。

曲線ヒットテスト on canvas
しかしあたり判定になると手段が提供されていないので自前でやる必要がある。以下任意のクリックした点が曲線上に位置しているかを計算する方法である。

1、曲線上の任意の点の接線の傾きを計算し曲線上の近傍点の接線の傾きと比較する
2、1の差が許容範囲内ならば2点は直線に近いと判断する
3、1の差が許容範囲外ならば2点間は曲がっているので2点間の中央で分割し再度1の判定を再帰的に行う
4、1~3までを繰り返し曲線を点で分割する
5、4で得られた点群とクリックされた点が直線上に位置するかを計算する
6、5で直線上に存在すれば曲線がクリックされたとする
7、5で直線上に存在しないならば曲線はクリックされていないとする

上記の計算に必要なものを解説していく。

曲線上の任意の点の計算方法
Cubic bezier curveの式を使うと簡単に任意の点を取得できる。Cubic bezier curveの式は以下。
b(t) = p0*(1-t)^3 + p1*3t(1-t)^2 + p2*3(1-t)t^2 + p3*t^3
p0は始点、p3が終点、p1、p2はそれぞれ制御点の1と2だ。これに時間であるtを0~1の間で指定すると曲線上の任意の点が取得できる。

曲線上の任意の点の接線の傾きの計算方法
これもCubic bezier curveの式の導関数を使えば簡単に計算できる。式は以下。
b'(t) = 3(1-t)^2(p1-p0) + 6(1-t)t(p2-p1) + 3t^2(p3-p2)
p0~p3とtの説明は前述したものと同じだ。

上2つの式はWikipediaを見てもらったほうが分かりやすいと思う。

任意の点が2点間の線上に存在するかの計算方法
任意の点Pが線AB上に存在するならば距離AB=距離AP+距離PBが成り立つ。

コード解説
ここから実際にいくつかコードを見ていこう。

まずは曲線上の任意の点を取得するコード。pointsは始点、制御点1、制御点2、終点の配列だ。
function b0(t) { return Math.pow(1 - t, 3); };
function b1(t) { return t * Math.pow(1 - t, 2) * 3; };
function b2(t) { return (1 - t) * Math.pow(t, 2) * 3; };
function b3(t) { return Math.pow(t, 3); };
function getPointOnBezier(points, t){
 var x = points[0].x * b0(t) + points[1].x * b1(t) + points[2].x * b2(t) + points[3].x * b3(t)
  , y = points[0].y * b0(t) + points[1].y * b1(t) + points[2].y * b2(t) + points[3].y * b3(t);
 return new Point(x, y);
};

ついで曲線上の任意の点の接線の傾きを取得するコード。ここもpointsは始点、制御点1、制御点2、終点の配列だ。
function bd0(t) { return 3 * Math.pow(1-t, 2); };
function bd1(t) { return 6 * (1-t) * t; };
function bd2(t) { return 3 * Math.pow(t, 2); };
function getSlopeOfTangentLine(points, t){
 var x = bd0(t) * (points[1].x-points[0].x) + bd1(t) * (points[2].x-points[1].x) + bd2(t) * (points[3].x-points[2].x)
  , y = bd0(t) * (points[1].y-points[0].y) + bd1(t) * (points[2].y-points[1].y) + bd2(t) * (points[3].y-points[2].y);
 return y/x;
};

曲線を直線に分割していくコード。mDiffやdDiffのしきい値をどこに設定するかで分割する大きさが変わってくるので上手いこと調整してもらいたい。
function calculateDividingPoints(){
 var points = []
  , bi = bezierInfo
  , i;
 for(i = 0.1; i <= 1.0; i += 0.1){
  divideCurveRecursively(bi, points, i-0.1, i); 
 }
 points.push(bi.points[bi.points.length-1]);
 bi.dividingPoints = points;
};
function divideCurveRecursively(bi, points, t1, t2){
 var prevD = getSlopeOfTangentLine(bi.points, t1)
  , currentD = getSlopeOfTangentLine(bi.points, t2)
  , dDiff = Math.abs(Math.abs(prevD)-Math.abs(currentD))
  , mDiff = t2-t1
  , middleT = t1 + mDiff / 2;
  
 if(mDiff > 0.05 && (sign(prevD) !== sign(currentD) || dDiff > 0.2)){
  divideCurveRecursively(bi, points, t1, middleT);
  divideCurveRecursively(bi, points, middleT, t2);
 }
 else
  points.push(getPointOnBezier(bi.points, t1));
};

任意の点が2点間の線上に存在するかをチェックするコード。これも許容値を変更することで当たり判定を厳しくしたりゆるくしたりできるので上手いこと調整してもらいたい。
function onStraightLine(pt){
 var bi = bezierInfo
  , i;
 for(i = 1; i < bi.dividingPoints.length; i ++){
  var p1 = bi.dividingPoints[i-1]
   , p2 = bi.dividingPoints[i]
   , trueDistance = calculateDistance(p1, p2)
   , testDistance1 = calculateDistance(p1, pt)
   , testDistance2 = calculateDistance(pt, p2);
  if(Math.abs(trueDistance - (testDistance1 + testDistance2)) < 0.2)
   return true;
 } 
 return false;
};

ざーっと解説してきたけれどCubic Bezierの公式さえ分かっていればさほど難しくないと思う。フレームワークから提供される曲線のあたり判定などは多量の曲線が存在する場合にボトルネックになったりもするので自前で計算できるよう公式を理解しておいても損はないだろう。処理実行の最適化などはまったく考慮していないので使用メモリだったり処理時間などを勘案して計算処理をキャッシュするなどは実際のコードベースでは行ったほうがよいと思われる。