GDC & シリコンバレーツアー
9leapの優勝賞品として、GDC & シリコンバレーツアーに行って来ました。
9leapは年間を通して行われるゲームのコンテストで、去年の春頃から行われていました。僕は[twitter:@SakeRice]さんとチームで参加していて、投稿していたうちの1本が最優秀賞として選ばれたので、代表ということでツアー参加の権利を譲ってもらいました。
まず最初は成田からサンフランシスコへ向かう飛行機の中でハッカソンがありました。UEIでは恒例行事になっているらしく、今年は「日本」というテーマで到着までに5本ゲームを作ることになりました。結構準備していったつもりだったんですが、実際にはあたふたしてしまい、3本で時間切れになってしまいました。社員の方々はきっちり5本作った上にひとつひとつの出来も良くて流石でした。
初日はまずComputer History Museumという施設に連れて行ってもらいました。Computer History Museumはコンピュータ史の博物館で、手回しの計算機からスパコンまで歴史に残る様々なコンピュータが展示されていました。
プログラミング言語の樹形図
Google通り。アメリカでは道を作ったら勝手に名前をつけていいそうです。
会社の周りでゾロゾロしてたら怒られそうな雰囲気だったので外から眺めるだけにしておきました。
次はフィッシャーマンズワーフにあるMusee Mecaniqueに行きました。Musee Mecaniqueは機械を使ったエンタメの歴史が展示してあり、大昔のからくりが展示してあります。
古代のエロゲー。覗くとスライドショーが見れる。
機内ハッカソンの疲れもあり、その後の記憶が虚ろなまま初日が終わりました。
二日目もハッカソンから始まりました。今回は二時間で一本制作して9leapで一番評価が高かった人が優勝というルールです。午後からは街を散策しました。
iPadの自販機。しかも売り切れ。
iPadミュージシャン
オレオブレンダーブラスター。破滅的に甘い。
サンフランシスコの寿司。左からDragon, Kamikaze, Spider
三日目はシリコンバレーを回りました。
マクドナルド
人狼村からの脱出
鉄板焼き。海外では鉄板の前でシェフがパフォーマンスしてくれることが一般的なようです。
三日目からいよいよGDCに参加します。隣では新iPadの発表会が行われていました。
GDCではゲームデザインのセッションを中心に聞いていましたが、英語だとスライドを追いかけるのがやっとで、哲学的な話になるとお手上げ状態でした。聞いたセッションは大体こんな感じです。
[GDC 2012]「SimCity」のリードデザイナーが教える,ゲームデザインの基礎 - 4Gamer.net
[GDC 2012]歴史に埋もれたゲームから新たなアイデアが生まれる。「TRS-80」のタイトルを紹介するセッションに参加してみた - 4Gamer.net
『GRAVITY DAZE』のビジュアルの秘密に迫る!【GDC 2012】 - ファミ通.com
[GDC 2012]“一生忘れられない1本”を作り上げるコツとは。「スペースインベーダー インフィニティジーン」の石田氏が語る5つの方法 - 4Gamer.net
【GDC 2012】和田康宏氏が語る「牧場物語」誕生秘話 - GAME Watch
[GDC 2012]48時間で制作された驚愕のゲーム「Glitchhiker」は,誕生から6時間後に消滅していた - 4Gamer.net
セッション間の時間は9minute coding battleをやってました。これがとてもエキサイティングで、その場で乱入してくる人が現れるなど、今回一番楽しかったと言ってもいいくらいでした。
他にも多くの企業がブースを出していました。
ネコミミ
ヘリで遊ぶshi3zさん
こんな感じでした。実際にはもっともっとあれやこれやと楽しいイベントがあったんですが、いざ振り返ってみるとほとんど写真を撮っていませんでした。もっと詳しいレポートはそのうちwise9などにアップされるのではないかと思います。9leapは今年も続行らしいので、GDCツアーに参加してみたい方は是非挑戦して下さい。
enchant.js入門
大学の授業で話すことになったのでenchant.jsの使い方を説明します.「授業でプログラミングを触ったことがある」くらいの人を想定しているつもりです.enchant.jsはjavascriptでゲームを開発するためのライブラリで,PCだけでなくiPhoneやAndroidなどのスマートフォンのブラウザでも動作するゲームを作ることができます.またコンテストが開催されており,作ったゲームを投稿することもできます.
本稿では
- 開発環境の構築
- 画像の表示
- 画像の移動
- キー入力による移動
- マップの表示
- ステージのスクロール
- キャラクタの動き
- マップとの当たり判定
という順序で最終的に付属のアクションゲームと同じようなものを作るところまでいきます.
ブラウザを用意する
制作したゲームを実行するためのブラウザを用意します.FirefoxやChromeなどモダンなブラウザならだいたいいけるみたいです.今回はFirefox4を使用します.
公式サイトよりダウンロード・インストールします.
http://mozilla.jp/firefox/
また,Firefoxで開発する場合Firebugというアドオンがあると便利なのでこれも導入します(Chromeだと同等の機能が標準で備わっています).
Firebug
ライブラリを導入する
リポジトリからパッケージをダウンロードします.
GitHub - wise9/enchant.js: A simple JavaScript framework for creating games and apps
パッケージを解凍し,examples/action下のindex.htmlをブラウザで開くとサンプルのゲームで遊べます.
テンプレートの作成
javascriptでゲームを開発する場合,まずブラウザに表示させるためのhtmlファイルが必要です.先ほど開いたindex.htmlを適当な場所にコピーして次のように書き換えてください.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>enchant</title> <script type="text/javascript"> console.log("hoge"); </script> <style type="text/css"> body { margin: 0; } </style> </head> <body> </body> </html>
ファイルを開く前に,ブラウザのコンソール画面を有効にします.Firefox + Firebugの場合[ツール] -> [Firebug] -> [Firebugを開く]でコンソールを表示できます.
この状態でファイルを開くと,コンソールにメッセージが表示されます.htmlではscriptタグで囲んだ部分をjavascriptとして評価します.この場合7行目がそれにあたり,console.logはコンソールにメッセージを表示させる関数です.
また,scriptタグ内に書いたコードはすぐに実行されますが,ページ内のまだ読み込まれていない要素にアクセスしたい場合すぐに実行されると困ります.全ての要素の読み込みが終わってからコードを実行したい場合次のようにします.
window.onload = function() { console.log("hoge"); }
javascrpitではfunctionで囲った部分が関数として評価され,window.onloadに代入した関数はページの読み込み後に実行されます.
スプライトの表示
実際にゲームのコードを書いていきます.まずダウンロードしたパッケージ内に入っていたenchant.jsとサンプルフォルダに入っていたchar1.gifをhtmlファイルと同じフォルダにコピーします.
enchant.jsを読み込むには,scriptタグのsrcにファイルパスを指定します.コードを次のように書き換えて実行するとクマが表示されます.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>enchant</title> <script type="text/javascript" src="enchant.js"></script> <script type="text/javascript"> enchant(); window.onload = function() { var game = new Game(320, 320); game.fps = 24; game.preload('chara1.gif'); game.onload = function() { var bear = new Sprite(32, 32); bear.image = game.assets['chara1.gif']; game.rootScene.addChild(bear); } game.start(); } </script> <style type="text/css"> body { margin: 0; } </style> </head> <body> </body> </html>
enchant()はライブラリを使用するための初期化処理で,enchant.jsを使用する際は必ず最初に呼び出します.
var game = new Game(320, 320); game.fps = 24; game.preload('chara1.gif');
ゲーム画面を320x320で作り,フレームレートを設定し,使用する画像を読み込んでいます.
game.onload = function() { ... }
window.onloadと同様に,game.onloadに関数を登録しておくと画像の読み込みや描画領域の確保などが終わったあとに登録した関数が実行されます.
var bear = new Sprite(32, 32); bear.image = game.assets['chara1.gif']; game.rootScene.addChild(bear);
画像を表示するにはSpriteを用意し,シーンに追加します.初期化時にスプライトのサイズを,imageプロパティにgame.assetsを通して読み込んだ画像を指定します.ここで,画像のサイズとスプライトのサイズが違う場合画像の左上が切り取られるような形で表示されます.
game.start();
最後にゲームを起動します.
画像の移動
表示した画像を移動させます.
bear.addEventListener(Event.ENTER_FRAME, function(e) { bear.x += 1; });
addEventListenerを使うとSpriteやSceneなどにイベントと処理のペアを登録することができます.ここでは,ENTER_FRAMEイベントにクマを移動させる処理を登録し,フレーム毎に徐々にクマの位置を移動させることでアニメーションを実現しています.他にも画面をタップ(PC/Macではクリック)したときのイベントなどがあります.イベント一覧はドキュメントのenchant.Eventで確認できます.
キー入力による移動
キー入力によってクマが移動するようにします.コードを次のように書き換えます.
bear.addEventListener(Event.ENTER_FRAME, function(e) { if(game.input.right) bear.x += 5; else if(game.input.left) bear.x -= 5; });
game.inputにはフレーム毎の入力の状態が保持されており,上記のコードではユーザのキー入力に応じてクマの位置を移動させています.
マップの表示
キャラクタのように激しく動きまわるものでなく,ステージや背景など比較的動かないものはMapを使うと楽に書けます.サンプルフォルダのmap2.gifをhtmlファイルと同じフォルダにコピーし,ソースコードを書き足します.
// preloadにも追加 game.preload("chara1.gif", "map2.gif");
var blocks = [ [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, ...(略.適宜サンプルからコピーしてください. ]; var map = new Map(16, 16); map.image = game.assets["map2.gif"]; map.loadData(blocks);
マップを生成するにはマップチップというものを使います.これは,マップを構成する画像を並べてひとつの画像にしたものです.Mapは初期化時にチップ1つのの大きさを,imageにマップチップの画像を指定します.チップの並びはloadDataに二次元配列を渡すことで設定します.配列内の数値はマップチップの座標に対応(下図)しており,何も無い部分は-1にします.
ステージのスクロール
クマの移動によってマップの表示されてない部分を見えるようにします.
var stage = new Group(); stage.addChild(map); stage.addChild(bear); stage.addEventListener(Event.ENTER_FRAME, function(e) { if(stage.x > 64 - bear.x) stage.x = 64 - bear.x; }); // rootSceneへのaddChildは書き換え game.rootScene.addChild(stage);
マップのスクロールはマップとクマをステージとしてひとつにまとめ,ステージ自体を動かすことで実現します.Groupは複数のマップやキャラクタをまとめるために使います.stageにもEventListenerを追加し,フレーム毎にクマの位置に応じてステージも動かします.
キャラクタの動き
キャラクタの動きを実装します.今までキー入力によって一定の速度で移動していたものを加速しながら動くようにします.bearのaddEventListenerを次のように書き換えます.
bear.vx = 0; bear.addEventListener(Event.ENTER_FRAME, function(e) { var ax = 0; if(game.input.right) ax += 0.5; if(game.input.left) ax -= 0.5; bear.vx += ax; bear.x += bear.vx; });
加速運動を表現するには速度と加速度が必要なので,それぞれvx,axとします.毎フレームごとにキー入力によって加速度を変更し,加速度を速度に,速度を位置に加算します.実行して右キーを押し続けるともの凄い速さでクマが飛んでいきます.これではゲームにならないので,速度に制限をもたせます.
bear.vx = Math.min(Math.max(bear.vx, -10), 10); bear.x += bear.vx;
Math.maxは引数の中で一番大きい値を返し,minは小さい値を返します.速度を位置に加算する直前にmaxとminを使って速度に上限と下限をもたせます.実行すると徐々に速くなっていきますが,一定の速度以上にはなりません.次に,キーを離すと減速するようにします.
if(game.input.right) ax += 0.5; if(game.input.left) ax -= 0.5; if (bear.vx > 0.3) ax -= 0.3; else if (bear.vx > 0) ax -= bear.vx; if (bear.vx < -0.3) ax += 0.3; else if (bear.vx < 0) ax -= bear.vx;
速度が一定以上の場合加速度を減らし,ほぼ止まりかけのときは決め打ちで止めます.逆向きも同じです.次に,クマのアニメーションを設定します.
// bear初期化時
bear.pose = 0;
var ax = 0; if(game.input.right) ax += 0.5; if(game.input.left) ax -= 0.5; if (ax > 0) bear.scaleX = 1; if (ax < 0) bear.scaleX = -1; if (ax != 0) { if (game.frame % 3 == 0) { bear.pose++; bear.pose %= 2; } bear.frame = bear.pose + 1; } else bear.frame = 0;
scaleXはキャラクタのX軸方向の拡大率を表し,これに-1を代入することで左右の表示を反転することができます.bear.frameはMapと同じように値によって表示する画像を変えることができます.ここでは,bearにposeプロパティを設定し,一定フレーム毎にその値を変え,frameに代入しています.game.frameはゲームが開始してからのフレーム数を返し,それを定数で割った余りを見ることで一定フレーム毎の処理を行っています.
ジャンプと重力
上下の運動を実装します.bearの初期化と位置の更新の前後を次のように書き換えます.
bear.vy = 0;
bear.jumping = false;
if(game.input.up && !bear.jumping) { bear.vy = -9; bear.jumping = true; } bear.vy += 0.5; bear.x += bear.vx; bear.y += bear.vy; if(bear.y >= 100) { bear.y = 100; bear.jumping = false; }
実行すると,上キーでジャンプします.まずジャンプ中かどうかを判定するフラグを用意し,フラグが立っていないかつ上キーが押されていた場合上方向へ速度を設定します.さらに,毎フレームごとに下方向へ速度を加算することで重力を表現します.位置の更新が終わった後,着地を判定します.
当たり判定
最後に,キャラとマップの当たり判定を書きます.速度を決定した後のコードを次のように書き換えます.
var dx = bear.x + bear.vx + 5; var dy = bear.y + bear.vy; if(map.hitTest(dx, dy + bear.height) || map.hitTest(dx + bear.width - 10, dy + bear.height)) { dy = Math.floor((dy + bear.height) / 16) * 16 - bear.height; bear.vy = 0; bear.jumping = false; } bear.x = dx - 5; bear.y = dy;
実行すると,一定の高さを基準にジャンプしていたのがマップに着地するようになりました.map.hitTestは引数にとったx,y座標にマップがあるかどうかを判定します.ここでは,bearの左下(dx, dy + bear.height)か右下(dx + bear.width, dy + bear.height)がマップに当たっていた場合,y座標に地面の高さを代入します(ちなみにxに5を足しているのは微調整です.width - 10も同様).Math.floorは引数にとった値の小数点を切り捨てた値を返すので,dyにはマップの境界(- bearの高さ)が代入されます.後は同様に上と左右の当たり判定も加えていきます.上と左右を加えていく際にはもう少し細かい処理が必要なのでここでは割愛しますが,基本的には同じです.詳しくはサンプルのソースを見てください.
おわりに
これで一通り完成しました.他にも http://9leap.net/ でenchant.jsを使ったゲームのソースが見れます.僕もenchant.jsを使って9leapにゲームを投稿したので,ぜひ遊んでみてください.
9leap : 合コンクエスト by blankblank - どこでも遊べる、投稿型ゲームサイト
参考
今回使ったソース
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>enchant</title> <script type="text/javascript" src="enchant.js"></script> <script type="text/javascript"> enchant(); window.onload = function() { var game = new Game(320, 320); game.fps = 24; game.preload("chara1.gif", "map2.gif"); game.onload = function() { var blocks = [ [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 4, 4, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 2, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3,-1,-1,-1,-1, 4, 4, 4, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3,-1,-1,-1,-1, 3, 3, 3, 3, 4, 4, 4, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 2, 2, 2, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 4, 4, 4, 4, 4, 4, 3, 3, 3,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,-1,-1,-1,-1, 4, 4, 4, 4, 4, 4, 4,-1,-1, 2, 2,-1,-1, 2, 2,-1,-1, 2, 2,-1,-1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], [ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3,10,10,10,10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,10,10,10,10, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1] ]; var map = new Map(16, 16); map.image = game.assets["map2.gif"]; map.loadData(blocks); var bear = new Sprite(32, 32); bear.image = game.assets["chara1.gif"]; bear.vx = 0; bear.vy = 0; bear.pose = 0; bear.jumping = false; bear.addEventListener(Event.ENTER_FRAME, function(e) { var ax = 0; if(game.input.right) ax += 0.5; if(game.input.left) ax -= 0.5; if (ax > 0) bear.scaleX = 1; if (ax < 0) bear.scaleX = -1; if (ax != 0) { if (game.frame % 3 == 0) { bear.pose++; bear.pose %= 2; } bear.frame = bear.pose + 1; } else bear.frame = 0; if (bear.vx > 0.3) ax -= 0.3; else if (bear.vx > 0) ax -= bear.vx; if (bear.vx < -0.3) ax += 0.3; else if (bear.vx < 0) ax -= bear.vx; bear.vx += ax; bear.vx = Math.min(Math.max(bear.vx, -10), 10); if(game.input.up && !bear.jumping) { bear.vy = -9; bear.jumping = true; } bear.vy += 0.5; var dx = bear.x + bear.vx + 5; var dy = bear.y + bear.vy; if(map.hitTest(dx, dy + bear.height) || map.hitTest(dx + bear.width - 10, dy + bear.height)) { dy = Math.floor(dy / 16) * 16; bear.vy = 0; bear.jumping = false; } bear.x = dx - 5; bear.y = dy; }); var stage = new Group(); stage.addChild(map); stage.addChild(bear); stage.addEventListener(Event.ENTER_FRAME, function(e) { if(stage.x > 64 - bear.x) stage.x = 64 - bear.x; }); game.rootScene.addChild(stage); } game.start(); } </script> <style type="text/css"> body { margin: 0; } </style> </head> <body> </body> </html>
AbstractActionの名前を変更する
putValueのキーにAction.NAMEを指定する。
import java.awt.FlowLayout; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.SwingUtilities; public class Hoge { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { AbstractAction a = new AbstractAction("hoge") { public void actionPerformed(ActionEvent e) { putValue(Action.NAME, "foo"); } }; JButton b = new JButton(a); JButton c = new JButton(a); JFrame f = new JFrame(); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setLayout(new FlowLayout()); f.add(b); f.add(c); f.pack(); f.setVisible(true); } }); } }
どっちのボタンを押しても両方の文字が変わる。
Sequencerの使い方
JavaでMIDIを扱う方法の解説。MacOSX(10.5.8), JDK1.5にて確認。
- 主に使用するクラス
- Sequencerを使って音を再生するサンプル
- SMFファイルからSequencerを生成する
- Sequence内の情報を所得
主に使用するクラス
Sequencer | 曲の再生を管理するクラス |
Sequence | 曲を表すクラス |
Track | トラックを表すクラス |
MidiEvent | MIDIメッセージとタイミングを表すクラス |
MidiMessage | MIDIメッセージを表すクラス |
Sequencerを使って音を再生するサンプル
import javax.sound.midi.MidiEvent; import javax.sound.midi.MidiSystem; import javax.sound.midi.Sequence; import javax.sound.midi.Sequencer; import javax.sound.midi.ShortMessage; import javax.sound.midi.Track; public class Hoge { public static void main(String[] args) { try { // デフォルトシーケンサの所得 Sequencer sequencer = MidiSystem.getSequencer(); // シーケンサの生成 // テンポベースのタイミング形式で // TicksPerBeatに480を指定 Sequence sequence = new Sequence(Sequence.PPQ, 480); // トラックを生成 Track track = sequence.createTrack(); // ノートオンイベントの生成 ShortMessage noteOn = new ShortMessage(); noteOn.setMessage(ShortMessage.NOTE_ON, 60, 127); MidiEvent e1 = new MidiEvent(noteOn, 480); // ノートオフイベントの生成 ShortMessage noteOff = new ShortMessage(); noteOff.setMessage(ShortMessage.NOTE_OFF, 60, 0); MidiEvent e2 = new MidiEvent(noteOff, 960); // イベントをトラックに追加する track.add(e1); track.add(e2); // シーケンスをシーケンサに追加する sequencer.setSequence(sequence); // 演奏開始 sequencer.open(); sequencer.start(); // 標準入力でブロック System.in.read(); // 終了 sequencer.stop(); sequencer.close(); } catch (Exception e) { e.printStackTrace(); } } }
SMFファイルからSequencerを生成する
既存のSMFファイルからSequencerを生成するにはMidiSystemのgetSequenceメソッドを使用する。
Sequence sequence = MidiSystem.getSequence(new File("hoge.mid"));
Sequence内の情報を所得
Track, MidiEventに順にアクセスする。
// シーケンス内のイベントを出力する for(Track t : sequence.getTracks()) { for(int i=0; i<t.size(); i++) { MidiEvent e = t.get(i); byte[] m = e.getMessage().getMessage(); System.out.println(e.getTick() + ", " + m[1] + ", " + m[2]); } }
このように所得したSequence内の情報は再生中に値を更新しても内容が即座に反映される。例えば先のサンプルで、演奏開始後にタイミングを変更すると
// 演奏開始 sequencer.open(); sequencer.start(); e1.setTick(1440); e2.setTick(1920);
再生が遅れる。
鍵盤を描く
public void paint(Graphics g) { super.paint(g); int w = getWidth() / 7; for(int i=0; i<7; i++) { g.drawLine(w * i, 0, w * i, getHeight()); if(BLACK_KEY[i]) g.fillRect(w * i + w / 2, 0, w, getHeight() / 2); } } private final boolean[] BLACK_KEY = {true, true, false, true, true, true, false};
これはよくない。
場所 | 長さ(cm) |
---|---|
白鍵 | 2.2 |
黒鍵 | 1.5 |
隙間 | 0.1 |
ミとファの隙間 | 0.2 |
ドとド#,レ#とミの間 | 1.3 |
ファ#とソ,ソ#とラの間 | 1.1 |
それ以外の間 | 1.2 |
わりと数字がそろっていないものの、白鍵を7分割、黒鍵を12分割で近似できそう。
public void paint(Graphics g) { super.paint(g); int w = getWidth() / 7; for(int i=0; i<7; i++) g.drawLine(w * i, 0, w * i, getHeight()); w = getWidth() / 12; for(int i=0; i<12; i++) { if(BLACK_KEY[i]) g.fillRect(w * i, 0, w, getHeight() / 2); } } private final boolean[] BLACK_KEY = {false, true, false, true, false, false, true, false, true, false, true, false};
微妙。ミまでとファからで分けて考えるのがよさそうだけどあんまりルール増やしすぎると押したとこ光らすとかが大変。
JFrameの内側のコンポーネントからサイズを決める場合
JFrameのsetSizeは、閉じるボタン等含めたフレーム自体の大きさを設定する。
import javax.swing.JFrame; import javax.swing.SwingUtilities; public class Hoge { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { JFrame f = new JFrame(); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setSize(256, 256); f.setVisible(true); } }); } }
内側の領域からサイズを決めたい場合は、内部のコンポーネントのsetPreferredSizeとJFrameのpackを呼び出す。
import java.awt.Dimension; import javax.swing.JFrame; import javax.swing.SwingUtilities; public class Hoge { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { JFrame f = new JFrame(); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.getContentPane().setPreferredSize(new Dimension(256, 256)); f.pack(); f.setVisible(true); } }); } }