SONICMOOV LAB

Herlock with CoffeeScript (簡単なシーン管理も作ってみた)

  • このエントリーをはてなブックマークに追加

この投稿はソニックムーブ Advent Calendar 201320日目の投稿になります。

どうも、ジョニーです。 過去にソニックのブログでTypeScriptの記事がありましたが、個人的にはCoffeeScriptの方が単純にコード量を少なくして書けるのと、インデント記法が好きなので勉強も兼ねてCoffeeScriptでHerlockを触ってみました。ついでに簡易的ではありますがシーン(ページ)遷移の管理クラスも作ってみたのでHerlockで何かアプリを作ってみようかなと思っている方の参考になれば幸いです。なお、Mac OSX 10.8環境で作業を行ったので適宜ご自身の環境に置き換えて読み進めて頂けたらと思います。

参考サイト

目次

  1. Node.js, Gruntをインストール
  2. Gruntfile.coffeeの設定
  3. コード説明
  4. CoffeeScriptを使ってみた感想

Node.js, Gruntをインストール

Macにnvm + Node.jsをインストールするGruntでCoffeeScriptを自動コンパイルするコピペを参考にNode.js, Gruntをインストールしてください。

Gruntfile.coffeeの設定

こちらもGruntでCoffeeScriptを自動コンパイルするコピペを参考に後のコード説明のところで紹介するGruntfile.coffeeの通りにファイルを設定してディレクトリ構造も同じようにしていただけたらあとは「grunt」コマンドを叩いてください。この状態で特定のディレクトリ内でCoffeeScriptファイルを作成すると自動的にJavascriptファイルも作成されるようになります。

コード説明

実行動画

プロジェクトはこちらに公開しているので手元にダウンロードするなりして参考にしながら読み進めて頂けたらと思います。

このプロジェクトを実行すると以下のようなフェードインフェードアウトを伴うシーン遷移アプリが実行されます。

Gruntfile.coffee

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

ここで行っているのはcoffeeディレクトリ内にcoffee拡張子のファイルが作成されたらappディレクトリ内に同じ構造でファイル(ディレクトリ)が作成されるように設定しています。

main.coffee

@_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)

このアプリが起動するときに一番最初に読み込むファイルはmain.jsとなります。
main.jsを自動コンパイルしてファイル作成するために main.coffeeを作ります。
ここでは以下の流れでプログラムが走っています。

  • このアプリの独自のグローバルオブジェクト(@_app)を作成。(@_appにグローバルからアクセス出来る物を詰め込む)
  • 各シーンのコンテナーとなるステージを作成して@_appに詰め込んでどこからでもシーンをステージに追加出来るようにする。
  • レイヤーを作成してwindowに追加して、作成したレイヤーに先ほど作ったステージを追加する。
  • requirejsを読み込む(依存関係を管理するライブラリ)
  • 一番最初に表示するシーン(MainScene)とシーン遷移の管理クラスであるAppManagerを読み込む。(AppManagerはシングルトン)
  • 初期画面表示用のメソッドであるAppScene#runWithSceneの引数にMainSceneのインスタンス渡してMainSceneを画面に表示する

requirejsの使い方の説明はここでは省略いたします。
本家サイトを参考にしていただけたらと思います。

AppManager.coffee

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

AppManagerはこのプロジェクトのシーン遷移周りの管理を司るクラスです。
シングルトンにして使う理由は現在表示しているシーンの管理や、今回は未実装ですが
シーン遷移のスタックを保持したいなどの要望をこのクラスでまとめるためです。
AppManager#runWithSceneは初回起動時に一度だけ呼ぶメソッドで、
既にシーンを保持しているときなど初回起動以外で呼ぶと例外を投げるようにしています。
実際にシーンからシーンに遷移したいときはAppManager#replaceSceneに
遷移したいシーンのインスタンスを引数に渡す事でシーン遷移する事が出来ます。

MainScene.coffee

_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

このファイルはBaseScene、GameScene、AppManagerに依存します。それぞれ依存するクラスの用途は以下の通りです。

  • BaseScene : シーンを作成するときに必ずBaseSceneを継承する
  • GameScene : 画面左上のボタン(色が違うところ)をタップしたときに遷移するシーン
  • AppManager : シーン遷移の管理クラス

AppManagerに関しては先ほども説明した通りシーン遷移のときに必ず使うクラスです。
今回は起動直後ではなくシーンからシーンへの遷移なのでAppManager#replaceSceneを使用しています。
GameSceneはMainSceneをほぼコピペしただけで、
シーンの背景色を変えたりrequirejsの循環参照を解決するためにrequireの呼び出し方を
イレギュラーな感じで使っているだけなのでここでは説明を省略します。
requirejsの循環参照の解決方法に関してはGameScene.coffeeの先頭に参考先を
コメントアウトして書いてあるのでそちらを参考にしていただけたらと思います。
BaseSceneは実際にシーンを画面に表示するときに色々とゴニョゴニョしてViewを画面に表示するように
@_app.stageにシーンを追加しています。
シーン遷移するときにただ単純に画面を切り替えるだけだと味気ないので
フェードイン/フェードアウトしてシーンを切り替えるようにしています。

BaseScene.coffee

_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

先ほどの紹介したMainSceneでも見かけたメソッドがここでも出てきていますが、
それぞれのメソッドはシーン遷移時のライフサイクル用のメソッドとなります。

  • onPrepare : 画面を表示する前にあらかじめ読み込みたいリソースを読むフェーズ(今回は未実装)
  • onEnter : onPrepareが終わったら呼ばれるフェーズ。ビューの表示の追加を行ったりすることを想定
  • onShow : onEnterが終わったら呼ばれるフェーズ。ここではフェードインアニメーションが開始する。
  • onHide : シーンが切り替わるのではなく一時的に非表示にしたいときを想定。今回は未実装
  • onLeave : AppManager#replaceSceneが呼ばれたときに呼び出される。フェードアウトアニメーションが開始する。
  • onDestroy : シーンを破棄するフェーズ。 onLeaveで呼び出したフェードアウトアニメーションが終了したら呼び出されてビューを破棄する

ライフサイクルとは別にサブクラスでも使われているメソッドにupdateが用意されています。
毎フレーム何か処理したい(アニメーションなど)がある場合はサブクラスのupdateメソッドをオーバーライドして
使うことを想定して作りました。
フェードイン/フェードアウトアニメーションを行っているメソッドであるfadein/fadeoutメソッドは
単純に15フレームで少しずつアルファの値を変えることで表現しています。

CoffeeScriptを使ってみた感想

CoffeeScriptは普段あまり使わないので実際にHerlockでCoffeeScriptを使うのは
最初は苦労するかなと思っていましたが、結構すんなり書けて良いなと思いました。
普段クラスの概念があるプログラミング言語でプログラムしている人にとってクラスが使えるのは
重要な点かと思います。
typescriptと違って型の定義とかは出来ませんが、少ない記述でインデントで記述出来る事によって
プログラムがすっきりする点だけでもCoffeeScriptを使ってみる価値は大いにあるのではないでしょうか。

実際に業務でも小規模な案件の時は積極的に使ってみようかな・・・

  • このエントリーをはてなブックマークに追加

記事作成者の紹介

ジョニー(フロントエンドエンジニア)

最近ずっとJavascriptばかりさわってたらJavascriptが好きになってきました。

関連するSONICMOOVのサービス

フロントエンドエンジニア募集中!

×

SNSでも情報配信中!ぜひご登録ください。

×

SNSでも
情報配信中!
SONICMOOV Facebookページ SONICMOOV Twitter
フロントエンドエンジニア募集中!

新着の記事

mautic is open source marketing automation