Herlock with CoffeeScript (簡単なシーン管理も作ってみた)
この投稿はソニックムーブ Advent Calendar 201320日目の投稿になります。
どうも、ジョニーです。 過去にソニックのブログでTypeScriptの記事がありましたが、個人的にはCoffeeScriptの方が単純にコード量を少なくして書けるのと、インデント記法が好きなので勉強も兼ねてCoffeeScriptでHerlockを触ってみました。ついでに簡易的ではありますがシーン(ページ)遷移の管理クラスも作ってみたのでHerlockで何かアプリを作ってみようかなと思っている方の参考になれば幸いです。なお、Mac OSX 10.8環境で作業を行ったので適宜ご自身の環境に置き換えて読み進めて頂けたらと思います。
目次
参考サイト
- GruntでCoffeeScriptを自動コンパイルするコピペ
- [改訂]CoffeeScriptでstatic/private/publicなメンバ/メソッドをもったクラスのつくりかた
- Macにnvm + Node.jsをインストールする
目次
Node.js, Gruntをインストール
Macにnvm + Node.jsをインストールする、GruntでCoffeeScriptを自動コンパイルするコピペを参考にNode.js, Gruntをインストールしてください。
Gruntfile.coffeeの設定
こちらもGruntでCoffeeScriptを自動コンパイルするコピペを参考に後のコード説明のところで紹介するGruntfile.coffeeの通りにファイルを設定してディレクトリ構造も同じようにしていただけたらあとは「grunt」コマンドを叩いてください。この状態で特定のディレクトリ内でCoffeeScriptファイルを作成すると自動的にJavascriptファイルも作成されるようになります。
コード説明
実行動画
プロジェクトはこちらに公開しているので手元にダウンロードするなりして参考にしながら読み進めて頂けたらと思います。
このプロジェクトを実行すると以下のようなフェードインフェードアウトを伴うシーン遷移アプリが実行されます。
Gruntfile.coffee
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[sourcecode lang="text"]module.exports = (grunt)-> grunt.initConfig pkg: grunt.file.readJSON 'package.json' watch: files: ['coffee/**/*.coffee'] tasks: 'coffee' coffee: compile: files: [ expand: true cwd: 'coffee/' src: ['**/*.coffee'] dest: 'app/' ext: '.js' ] grunt.loadNpmTasks 'grunt-contrib-coffee' grunt.loadNpmTasks 'grunt-contrib-watch' grunt.registerTask 'default', ['watch'] return[/sourcecode] |
ここで行っているのはcoffeeディレクトリ内にcoffee拡張子のファイルが作成されたらappディレクトリ内に同じ構造でファイル(ディレクトリ)が作成されるように設定しています。
main.coffee
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[sourcecode lang="text"]@_app = {} @_app.stage = new Stage( 640, 640 * window.innerHeight / window.innerWidth ) @_app.baseUrl = './coffee_sample/' # appは既にHerlockで使われている変数のため先頭に「_」をつける _app = @_app; window.addLayer( new Layer( _app.stage ) ) script = new Script( _app.baseUrl + 'lib/require.min.js'); script.onload = -> require.config({ baseUrl : _app.baseUrl, waitSeconds : 120, }) require ['app/scenes/MainScene', 'app/managers/AppManager'], (MainScene, AppManager) -> appManager = AppManager.getInstance() mainScene = new MainScene() appManager.runWithScene(mainScene)[/sourcecode] |
このアプリが起動するときに一番最初に読み込むファイルはmain.jsとなります。
main.jsを自動コンパイルしてファイル作成するために main.coffeeを作ります。
ここでは以下の流れでプログラムが走っています。
- このアプリの独自のグローバルオブジェクト(@_app)を作成。(@_appにグローバルからアクセス出来る物を詰め込む)
- 各シーンのコンテナーとなるステージを作成して@_appに詰め込んでどこからでもシーンをステージに追加出来るようにする。
- レイヤーを作成してwindowに追加して、作成したレイヤーに先ほど作ったステージを追加する。
- requirejsを読み込む(依存関係を管理するライブラリ)
- 一番最初に表示するシーン(MainScene)とシーン遷移の管理クラスであるAppManagerを読み込む。(AppManagerはシングルトン)
- 初期画面表示用のメソッドであるAppScene#runWithSceneの引数にMainSceneのインスタンス渡してMainSceneを画面に表示する
requirejsの使い方の説明はここでは省略いたします。
本家サイトを参考にしていただけたらと思います。
AppManager.coffee
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
[sourcecode lang="text"]define [], -> _app = @_app # private class class _AppManager constructor : -> # private member currentScene = null sceneStack = [] setScene = (scene) -> currentScene = scene scene.onEnter() scene.onShow() # シーン切り替え系 ------------------------- @runWithScene = (scene) -> if currentScene != null throw new Error('既にシーンをセットしている') setScene(scene) @replaceScene = (scene) -> currentScene.onLeave() # currentScene.onDestroy() setScene(scene) # public class class AppManager # private static instance instance = null # public static instance constructor : -> throw new Error('newで作成出来ない') @getInstance = -> if instance == null instance = new _AppManager() return instance return AppManager[/sourcecode] |
AppManagerはこのプロジェクトのシーン遷移周りの管理を司るクラスです。
シングルトンにして使う理由は現在表示しているシーンの管理や、今回は未実装ですが
シーン遷移のスタックを保持したいなどの要望をこのクラスでまとめるためです。
AppManager#runWithSceneは初回起動時に一度だけ呼ぶメソッドで、
既にシーンを保持しているときなど初回起動以外で呼ぶと例外を投げるようにしています。
実際にシーンからシーンに遷移したいときはAppManager#replaceSceneに
遷移したいシーンのインスタンスを引数に渡す事でシーン遷移する事が出来ます。
MainScene.coffee
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
[sourcecode lang="text"]_app = @_app; define ['app/scenes/BaseScene', 'app/scenes/GameScene', 'app/managers/AppManager'], (BaseScene, GameScene, AppManager) -> class MainScene extends BaseScene constructor : -> super onPrepare : -> super onEnter : -> super @setTitle 'MainScene' onShow : -> super @backgroundColor(0xff00ffff) button = new Sprite() bmp = new Bitmap( new BitmapData(1, 1, true, 0xffff00ff) ) bmp.width = bmp.height = 100 button.addChild(bmp) @addChild(button) button.addEventListener 'touchTap', (e) -> appManager = AppManager.getInstance() gameScene = new GameScene() appManager.replaceScene(gameScene) onHide : -> super onLeave : -> super onDestroy : -> super update : -> super return MainScene[/sourcecode] |
このファイルはBaseScene、GameScene、AppManagerに依存します。それぞれ依存するクラスの用途は以下の通りです。
- BaseScene : シーンを作成するときに必ずBaseSceneを継承する
- GameScene : 画面左上のボタン(色が違うところ)をタップしたときに遷移するシーン
- AppManager : シーン遷移の管理クラス
AppManagerに関しては先ほども説明した通りシーン遷移のときに必ず使うクラスです。
今回は起動直後ではなくシーンからシーンへの遷移なのでAppManager#replaceSceneを使用しています。
GameSceneはMainSceneをほぼコピペしただけで、
シーンの背景色を変えたりrequirejsの循環参照を解決するためにrequireの呼び出し方を
イレギュラーな感じで使っているだけなのでここでは説明を省略します。
requirejsの循環参照の解決方法に関してはGameScene.coffeeの先頭に参考先を
コメントアウトして書いてあるのでそちらを参考にしていただけたらと思います。
BaseSceneは実際にシーンを画面に表示するときに色々とゴニョゴニョしてViewを画面に表示するように
@_app.stageにシーンを追加しています。
シーン遷移するときにただ単純に画面を切り替えるだけだと味気ないので
フェードイン/フェードアウトしてシーンを切り替えるようにしています。
BaseScene.coffee
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
[sourcecode lang="text"]_app = @_app; define [], () -> class BaseScene FADEIN_FRAME = 15 FADEOUT_FRAME = 15 # private method --------------------------------------- # シーン遷移時アニメーション fadeout = (context, displayObject, callback) -> startAlpha = displayObject.alpha setAlpha = 0 currentFrame = 0 update = -> setAlpha = startAlpha - (++currentFrame / FADEOUT_FRAME) if setAlpha <= 0 setAlpha = 0 displayObject.removeEventListener 'enterFrame', update if callback callback.call context @alpha = setAlpha displayObject.addEventListener 'enterFrame', update fadein = (context, displayObject, callback) -> startAlpha = displayObject.alpha setAlpha = 0 currentFrame = 0 update = -> setAlpha = startAlpha + (++currentFrame / FADEIN_FRAME) if setAlpha >= 1 setAlpha = 1 displayObject.removeEventListener 'enterFrame', update if callback callback.call context @alpha = setAlpha displayObject.addEventListener 'enterFrame', update # ------------------------------------------------------ constructor : -> @view = new Sprite() bmd = new BitmapData(1, 1, true, 0xffff0000) @background = new Bitmap(bmd) @background.width = _app.stage.stageWidth @background.height = _app.stage.stageHeight @view.addChild(@background) @view.addEventListener 'enterFrame', @update @mouseEnabled(false) @title = null @setTitle('BaseScene') @view.alpha = 0 setWidth : (width) -> @background.width = width setHeight : (height) -> @background.height = height setTitle : (title) -> if !@title @title = new TextField() @title.defaultTextFormat = new TextFormat("", 48, 0x000000, true, false) @addChild(@title) @title.text = title @title.width = @title.textWidth @title.height = @title.textHeight @title.x = (_app.stage.stageWidth - @title.textWidth) / 2 backgroundColor : (colorHex) -> bmd = new BitmapData(1, 1, true, colorHex) @background.bitmapData = bmd addChild : (displayObject) -> @view.addChild(displayObject) mouseEnabled : (flag) -> @view.mouseEnabled = @view.mouseChildren = flag update : -> #lifecycle ----------------------------------------- # onEnterの前にあらかじめ読み込んでおきたいリソースなどをこのメソッドで呼び出す想定 onPrepare : -> # TODO onPrepareの行うと想定される処理が完了してから@onEnterを呼ぶようにする (completePrepare的なメソッドを作る) # runWithScene/replaceSceneが呼ばれたときに呼ばれる onEnter : -> @onPrepare() _app.stage.addChildAt(@view, 0) # onEnterで一通り処理が終わったら呼ばれる onShow : -> fadein @, @view, => @mouseEnabled(true) # シーンが切り替わるのではなくただ見えなくなるだけのときに呼ばれる想定で実装 onHide : -> # replaceSceneが呼ばれたときに呼ばれる onLeave : -> @mouseEnabled(false) fadeout @, @view, @onDestroy # console.log 'BaseScen leave' # @onDestroy() # replaceSceneが呼ばれたときに呼ばれる onDestroy : -> while @view.numChildren > 0 child = @view.removeChildAt(0) child = null _app.stage.removeChild(@view) @view = null return BaseScene[/sourcecode] |
先ほどの紹介したMainSceneでも見かけたメソッドがここでも出てきていますが、
それぞれのメソッドはシーン遷移時のライフサイクル用のメソッドとなります。
- onPrepare : 画面を表示する前にあらかじめ読み込みたいリソースを読むフェーズ(今回は未実装)
- onEnter : onPrepareが終わったら呼ばれるフェーズ。ビューの表示の追加を行ったりすることを想定
- onShow : onEnterが終わったら呼ばれるフェーズ。ここではフェードインアニメーションが開始する。
- onHide : シーンが切り替わるのではなく一時的に非表示にしたいときを想定。今回は未実装
- onLeave : AppManager#replaceSceneが呼ばれたときに呼び出される。フェードアウトアニメーションが開始する。
- onDestroy : シーンを破棄するフェーズ。 onLeaveで呼び出したフェードアウトアニメーションが終了したら呼び出されてビューを破棄する
ライフサイクルとは別にサブクラスでも使われているメソッドにupdateが用意されています。
毎フレーム何か処理したい(アニメーションなど)がある場合はサブクラスのupdateメソッドをオーバーライドして
使うことを想定して作りました。
フェードイン/フェードアウトアニメーションを行っているメソッドであるfadein/fadeoutメソッドは
単純に15フレームで少しずつアルファの値を変えることで表現しています。
CoffeeScriptを使ってみた感想
CoffeeScriptは普段あまり使わないので実際にHerlockでCoffeeScriptを使うのは
最初は苦労するかなと思っていましたが、結構すんなり書けて良いなと思いました。
普段クラスの概念があるプログラミング言語でプログラムしている人にとってクラスが使えるのは
重要な点かと思います。
typescriptと違って型の定義とかは出来ませんが、少ない記述でインデントで記述出来る事によって
プログラムがすっきりする点だけでもCoffeeScriptを使ってみる価値は大いにあるのではないでしょうか。
実際に業務でも小規模な案件の時は積極的に使ってみようかな・・・