はじめに
【WEBサービス】youtubeを使った音ゲー×タッチタイピングサービスを作ってみた【つくってみた】 - ぼくのかんがえたさいきょうのうぇぶさーびす
上記の記事で書いた「typebeats」のゲーム部を簡素化して説明してみます。 削っていくと100行に入りそうだったので詰め込んでみました。 かなり簡素化していますが、基本的な考え方は共通です。
ゲーム概要
リズムに合わせて降ってくるアイコンをタッチ/クリックするゲームです。 画面をタッチ/クリックするとゲームが開始します。
こいつが
こいつに
重なるくらいがタッチ/クリックするタイミングです。
スマホでの動作は確認していません。たぶん動かない気がします。
言語はCoffeeScript、フレームワークはenchant.jsです。 触れたことのない方はどっとインストールも参考にしてください。
CoffeeScript入門 (全13回) - プログラミングならドットインストール
enchant.js入門 (全12回) - プログラミングならドットインストール
リポジトリとデモ
リポジトリ
bokuweb/youtube_music_game · GitHub
デモ
http://bokuweb.github.io/youtube_music_game/
準備するもの
今回必要なものは以下のようになります。
- ブラウザ(chromeを推奨)
- エディタ(CoffeeScriptのシンタックスハイライトができるものがおすすめ)
用語
項目 | 説明 |
---|---|
ノート | リズムに合わせて落下してくるオブジェクト |
ラベル | 文字を表示する部品 |
シーン | タイトル画面、ゲーム画面、ポーズ画面、ゲームオーバー画面など、「○○画面」と呼ばれる単位 *1 |
FPS | Frame Per Sec 1秒間に何回画面が更新されるか |
*1 http://www.atmarkit.co.jp/ait/articles/1304/01/news034.htmlより ここではゲーム画面しかないのでシーンと言ったらゲーム画面のことです。
ゲーム詳細
ノートがタッチ/クリックされるべきタイミングを_timing
に格納しておき、この時間から逆算して、ノートをシーンに追加したり、動かしたりさせています。
enchant.jsの機能で指定したFPSの周期で動作させるメソッドを登録できますので、この登録したメソッド内で何を行うべきなのか判定し、実行するという形になります。
ゲームクラス定義部
ここではclass内で共通して参照する変数などを定義しておきます。
class @Game # ゲームクラスを宣言します。 # class名に@を付けることでglobalスコープに配置され、別ファイルから参照することができるようになります。 YOUTUBE_ID = 'HNYkOJ-T63k' # 再生する動画のIDを設定します。 _game = null # echant.jsのゲームコアを格納しておくprivate変数です。 _yt = null # youtube API制御classのインスタンスを格納しておく変数です。 _judge = null # タイミング判定を表示するラベルを格納しておく変数です。 # 落下してくるオブジェクトのタッチ/クリックタイミングを定義します。単位は[sec]です。 # ここに定義してある時間と実際にタッチ/クリックした時間を比較し、「GREAT」、「BAD」などの判定を行います。 _timing = [6.14,7.486,8.155,9.977,10.377,11.611,12.062,12.765,13.583,13.945,14.223,14.707,15.059,16.241,16.577,17.425,20.186,20.917,21.593,22.313,22.449,23.123,24.297,24.965,25.113,25.464,26.148,26.635,27.294,28.103,30.910,31.601,32.305,33.024,34.054,34.786,35.360,36.140,37.028,38.402,38.829,39.129,40.354,41.051,41.553,42.233,43.043,43.729,44.261,45.705,46.448,47.416,48.407,50.158,51.310,52.363,53.031,54.417,55.288,55.640,56.472,57.190,58.110,59.095,59.648,60.776, 61.993, 62.370, 63.072, 63.808, 64.493, 65.111, 65.688, 66.414, 67.192, 68.891, 69.209, 69.918, 70.056, 71.111, 71.744, 72.428, 72.861, 73.263, 73.755, 74.099, 74.639, 75.090, 75.426, 75.941, 76.472, 76.992] _timingIndex = 0 # _timing配列用のindexです _status = "stop" # ゲームのステータスです _endTime = 80 # ゲームの終了時間を定義します。 # ここでは再生時間が80secを超えたら終了処理に入ります。
コンストラクタ
クラスからnewでオブジェクトを作成した際に、自動的に実行されます。 このサンプルでは一番最後の行でnewしていますのでその時に実行されます。 constructor内では以下の処理を行っています。
- ゲーム本体の生成
- FPSの設定
- 事前にロードするリソースの設定
- ゲームの開始
constructor : (parms)-> enchant() # enchant.jsを使うためのおまじないです _game = new Core(800, 600) # ゲーム本体を生成します。Coreの引数はゲームのwidthとheightです。 # ここでは800×600のゲームを生成しています。 _game.fps = 30 # frame per sec つまり1秒当たり何回描画するかを設定します。 _game.preload("icon.png", "shadow.png") # 使用する画像ファイルをpreloadするよう設定しておきます。 _game.start() # ゲームを開始します。 # ただ、いきなりゲームが開始されるわけではなくまずは画像のロードが行われます。 # ロードが完了すると_game.onloadメソッドが呼ばれます。
ロード完了時の処理
ロードが完了すると_game.onloadメソッドが呼ばれます。 onloadメソッド内では以下の処理を行っています。
- ゲームタッチ/クリック時の処理を登録
- 動画の埋め込み
- 判定ラベルの追加
- 着地ポイントのs追加
_game.onload = -> # 画像等のロードが完了すると実行されるメソッドです # ゲーム画面をタッチ/クリックしたときの処理を登録しておきます。 _game.rootScene.addEventListener "touchstart", (e)-> if _yt.isReady() # youtubeプレイヤーが準備できている? _game.rootScene.addEventListener "enterframe", _proccesRootSceneFrame # 30FPSすなわち1/30 = 33.3333..ms周期で呼び出されるメソッドを登録しておきます。 # 33.3333..ms経過するたびに_proccesRootSceneFrameメソッドがコールされます。 _status = "playing" # ステータスをplay中に変更 _yt.play() # youtube動画を再生します。 # 動画を埋め込みます video = new Entity() video._element = document.createElement('div') video.x = 500 # 動画設X置座標 video.y = 300 # 動画設置Y座標 # iframeタグを埋め込みます。 # https://www.youtube.com/embed/HNYkOJ-T63k?enablejsapi=1..のHNYkOJ-T63kが動画IDです。 # ここを変更することで動画を変更できます。 # その他詳細はhttps://developers.google.com/youtube/js_api_reference?hl=jaを参照してください。 video._element.innerHTML = '<iframe src="https://www.youtube.com/embed/'+YOUTUBE_ID+'?enablejsapi=1&controls=0&showinfo=0&autoplay=0&rel=0&vq=small" width="300" height="200" frameborder="0" id="player"></iframe>' _game.rootScene.addChild(video) # 動画をシーンに追加 _yt = new Yt() # youube制御インスタンスを生成 # 「GREAT」などの判定結果を表示するラベルを生成 _judge = new Label() _judge.font = "36px Arial" # サイズ、フォントを指定 _judge.x = 100 # X座標 _judge.y = 100 # Y座標 _game.rootScene.addChild(_judge) # シーンに追加 # 落下してくるオブジェクトの着地ポイントを示すイメージを設置する shadow = new Sprite(80, 80) shadow.image = _game.assets["shadow.png"] # 画像ファイルを指定 shadow.x = 100 # X座標 shadow.y = 380 # Y座標 _game.rootScene.addChild(shadow) # シーンに追加
ノート生成部
_isNoteGenerateTiming
はノートを作成するかどうか問い合わせるメソッドです。
返り値がtrueの場合ノートを生成するタイミング、falseの場合は生成するタイミングではないことを示します。
動画の再生時間がタッチ/クリックタイミングから1秒減算した値より大きければtrueを返します。
すなわちタッチ/クリックタイミングから逆算して約1秒前にノートを生成しています。
_isNoteGenerateTiming = -> if _timing[_timingIndex]? # 配列_timingにデータが存在するかしらべます。 if _yt.getCurrentTime() > _timing[_timingIndex] - 1 # タッチ/クリックタイミングの1秒前? return true return false
_generateNote
は引数で指定された番号のノートを作成し、ノートの動作設定、33.3333..ms周期(すなわち30FPS)で呼び出されるメソッドを登録、ノートをタッチ/クリックしたとき呼び出されるメソッドを登録を行います。
_generateNote = (number)-> # ノートの生成部 note = new Sprite(80, 80) note.image = _game.assets["icon.png"] note.number = number note.x = 100 note.y = -100 # 画面外に配置 note.timing = _timing[number] #タッチ/クリックタイミングnote.timingに設定しておく _game.rootScene.addChild(note) # シーンに追加 # ノートがどのように動作するのかを記述します。 note.tl.setTimeBased() # tl.setTimeBased()を実行することでenchant.jsのアニメーションを時間ベースで実行することができます。 # デフォルトはフレームベースであるため「何秒間でここからここまで移動」というような処理ができないためです。 # ノートのY座標を現在の位置(すなわち-100px)から380pxまで```_timing[number] - _yt.getCurrentTime()) * 1000```msecかけて移動するよう設定しています。 # ```_timing[number]```という目標時間から```_yt.getCurrentTime()```という現在時間の差を求めどれだけの時間で移動すべきかを計算しています。 note.tl.moveY(380, (_timing[number] - _yt.getCurrentTime()) * 1000) # ノートをタッチ/クリックしたとき呼び出されるメソッドを登録を行います。。 note.addEventListener "touchstart", (e)-> @clearTime = _yt.getCurrentTime() # タッチ/クリック時の時間をclearTimeとしてnoteのプロパティに登録しておきます。 @clear = true # クリアフラグをたてます。 # 33.3333..ms周期(すなわち30FPS)で呼び出されるメソッドを登録 note.addEventListener "enterframe", -> # タッチ/クリックタイミングから1秒以上経過していればノートをシーンより削除します。 if _yt.getCurrentTime() > _timing[@number] + 1 then _game.rootScene.removeChild(@) # クリアフラグが立っていれば if @clear # ここでノートをタッチ/クリックしたときの処理を記載します。 # 以下の処理も33.3333..ms周期で実行されるため、33.3333..ms毎にノートを透明にしていく処理とノートを大きくする処理を記述しています。 @opacity -= 0.2 # 不透明度を減らしていきます。 @scale(@scaleX + 0.05, @scaleY + 0.05) # 5%ずつスケールアップさせる # ノートが完全に透明になったら if @opacity <= 0 _game.rootScene.removeChild(@) # シーンから削除します。 # ```note.addEventListener "touchstart", (e)->```内で登録したタッチ/クリック時の時間とタッチ/クリックタイミングを比較し±0.2秒であれば"COOL"、±0.4秒であれば、"GOOD"、それ以外は"BAD"としています。 if -0.2 <= @clearTime - _timing[@number] <= 0.2 then _judge.text = "COOL" else if -0.4 <= @clearTime - _timing[@number] <= 0.4 then _judge.text = "GOOD" else _judge.text = "BAD"
メインフレーム部
_proccesRootSceneFrame
は33.3333..ms周期(すなわち30FPS)で呼び出されるメソッドです
_game.onload
内で登録されています。
処理内容は以下です。
- ノートを生成すべきであればノートを生成
- 再生時間が終了時間を超えていればゲーム終了処理を行う
_proccesRootSceneFrame = -> if _status is "playing" if _isNoteGenerateTiming() _generateNote(_timingIndex) _timingIndex++ # 再生時間が終了時間以上? if _yt.getCurrentTime() >= _endTime # 動画のボリュームを落としていく。 _yt.setVolume(_youtube.getVolume() - 1) # ボリュームが0になったら再生終了し、ステータスを"end"に設定 if _yt.getVolume() <= 0 _yt.stop() _status = "end"
youtube制御クラス
動画制御クラスですが、公式のサンプルをclassに閉じ込めただけなので詳細は省きます。 詳細はhttps://developers.google.com/youtube/js_api_reference?hl=jaを参照してください。 以下のメソッドで動画を制御可能です。
- play : 動画を再生
- getCurrentTime : 再生時間を取得
- setVolume : 音量を設定
- getVolume : 現在の音量を取得
- isReady : Youtubeプレイヤーの準備ができているか問い合わせる
class @Yt _player = null _isReady = false _state = null constructor : (parms)-> tag = document.createElement('script') tag.src = 'https://www.youtube.com/iframe_api' firstScriptTag = document.getElementsByTagName('script')[0] firstScriptTag.parentNode.insertBefore(tag, firstScriptTag) play : -> _player.playVideo() getCurrentTime : -> _player.getCurrentTime() setVolume : (volume)-> _player.setVolume(volume) getVolume : -> _player.getVolume() isReady : -> _isReady onPlayerReady = -> _isReady = true window.onYouTubeIframeAPIReady = -> _player = new YT.Player 'player', events: 'onReady': onPlayerReady
実行部
newすることにより、constructor
、_game.onload
の順にコールされ、ゲームが開始されます。
new Game()
コンパイル
書き終えたらmain.coffeeをコンパイルしてください。 main.jsが生成されますのでこれをhtmlファイルで読み込みます。 コンパイルは公式サイトの「TRY COFFEESCRIPT」からも可能です。たぶん。
html
<html> <head> <meta charset="utf-8"> </head> <body> <div id="enchant-stage"></div> <script type="text/javascript" src="enchant.js"></script> <!-- enchant.jsは予めダウンロードしておいてください--> <script type="text/javascript" src="main.js"></script> <!-- 今回書いたものをコンパイルしたもの --> </body> </html>
注意点など
この程度であればなんとか動作すると思いますが、規模が大きくなってくると現状の作りではパフォーマンス面で苦しくなってくるかと思います。 簡素化のため現状メモリの管理はまったく気にせず、好き放題やっていますが、このようなつくりの場合GC(ガベージコレクション)が、がしがし走るかとおもいます。 GCが走っている間はユーザのプログラムは止まった状態となるので、動きがカクカクしたり、一瞬止まったりする原因になります。 ここでは割愛(というか自分もよくわかっていない)しますがゲームを作る上では必要な知識となります。 本格的に勉強される方は以下の記事も参考にしてみてください。
wise9 › enchant.jsで3Dゲームを作ってみる!
オブジェクトプールを使った静的メモリ JavaScript - HTML5 Rocks
スプライトをプールして使い回す - enchant.jsのTipsを集めるWiki
ソース
main.coffee
class @Game YOUTUBE_ID = 'HNYkOJ-T63k' _game = null _yt = null _judge = null _timing = [6.14,7.486,8.155,9.977,10.377,11.611,12.062,12.765,13.583,13.945,14.223,14.707,15.059,16.241,16.577,17.425,20.186,20.917,21.593,22.313,22.449,23.123,24.297,24.965,25.113,25.464,26.148,26.635,27.294,28.103,30.910,31.601,32.305,33.024,34.054,34.786,35.360,36.140,37.028,38.402,38.829,39.129,40.354,41.051,41.553,42.233,43.043,43.729,44.261,45.705,46.448,47.416,48.407,50.158,51.310,52.363,53.031,54.417,55.288,55.640,56.472,57.190,58.110,59.095,59.648,60.776, 61.993, 62.370, 63.072, 63.808, 64.493, 65.111, 65.688, 66.414, 67.192, 68.891, 69.209, 69.918, 70.056, 71.111, 71.744, 72.428, 72.861, 73.263, 73.755, 74.099, 74.639, 75.090, 75.426, 75.941, 76.472, 76.992] _timingIndex = 0 _status = "stop" _endTime = 80 constructor : (parms)-> enchant() _game = new Core(800, 600) _game.fps = 30 _game.preload("icon.png", "shadow.png") _game.start() _game.onload = -> _game.rootScene.addEventListener "touchstart", (e)-> if _yt.isReady() _game.rootScene.addEventListener "enterframe", _proccesRootSceneFrame _status = "playing" _yt.play() video = new Entity() video._element = document.createElement('div') video.x = 500 video.y = 300 video._element.innerHTML = '<iframe src="https://www.youtube.com/embed/'+YOUTUBE_ID+'?enablejsapi=1&controls=0&showinfo=0&autoplay=0&rel=0&vq=small" width="300" height="200" frameborder="0" id="player"></iframe>' _game.rootScene.addChild(video) _yt = new Yt() _judge = new Label() _judge.font = "36px Arial" _judge.x = 100 _judge.y = 100 _game.rootScene.addChild(_judge) shadow = new Sprite(80, 80) shadow.image = _game.assets["shadow.png"] shadow.x = 100 shadow.y = 380 _game.rootScene.addChild(shadow) _isNoteGenerateTiming = -> if _timing[_timingIndex]? if _yt.getCurrentTime() > _timing[_timingIndex] - 1 return true return false _generateNote = (number)-> note = new Sprite(80, 80) note.image = _game.assets["icon.png"] note.number = number note.x = 100 note.y = -100 note.timing = _timing[number] _game.rootScene.addChild(note) note.tl.setTimeBased() note.tl.moveY(380, (_timing[number] - _yt.getCurrentTime()) * 1000) note.addEventListener "touchstart", (e)-> @clearTime = _yt.getCurrentTime() @clear = true note.addEventListener "enterframe", -> if _yt.getCurrentTime() > _timing[@number] + 1 then _game.rootScene.removeChild(@) if @clear @opacity -= 0.2 @scale(@scaleX + 0.05, @scaleY + 0.05) if @opacity <= 0 _game.rootScene.removeChild(@) if -0.2 <= @clearTime - _timing[@number] <= 0.2 then _judge.text = "COOL" else if -0.4 <= @clearTime - _timing[@number] <= 0.4 then _judge.text = "GOOD" else _judge.text = "BAD" _proccesRootSceneFrame = -> if _status is "playing" if _isNoteGenerateTiming() _generateNote(_timingIndex) _timingIndex++ if _yt.getCurrentTime() >= _endTime _yt.setVolume(_youtube.getVolume() - 1) if _yt.getVolume() <= 0 _yt.stop() _status = "end" class @Yt _player = null _isReady = false _state = null constructor : (parms)-> tag = document.createElement('script') tag.src = 'https://www.youtube.com/iframe_api' firstScriptTag = document.getElementsByTagName('script')[0] firstScriptTag.parentNode.insertBefore(tag, firstScriptTag) play : -> _player.playVideo() getCurrentTime : -> _player.getCurrentTime() setVolume : (volume)-> _player.setVolume(volume) getVolume : -> _player.getVolume() isReady : -> _isReady onPlayerReady = -> _isReady = true window.onYouTubeIframeAPIReady = -> _player = new YT.Player 'player', events: 'onReady': onPlayerReady new Game()
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <div id="enchant-stage"></div> <script type="text/javascript" src="enchant.js"></script> <script type="text/javascript" src="main.js"></script> </body> </html>
その他
もう少しわかりやすくして、おっ立ち野郎 (id:ottati)さんのコードレシピ - 初心者のための簡単プログラミングレシピサイトに登録させてもらおう。