AnimateCCで “ブロック崩し” を作ってみるよ![ドラッグ操作pressmove] [当たり判定hitTest] [子の追加addChild] [ムービークリップの幅高さnominalBounds] HTML5 Canvas入門(スマホ対応)CreateJS

Animate CC で “ブロック崩し” のサンプルを作ってみるよ!

今回はゲーム作りです。いくつも動く要素があって、状況が刻一刻と変化するwebアプリを作りましょう。

↓完成したサンプルです↓
タップすると画面に表示されます
003breakout ver20181206

  • [START]をクリックしてゲーム開始
  • 画面のどこでもドラッグ(スワイプ)するとラケットが動きます
  • すべてのブロックを崩すとクリア
  • ボールが画面の下に落ちたらゲームオーバー

基本的な構成を考える

とりあえず今回はブロック崩しに最低限必要な要素のみを作りたいと思います。本来ならスコア表示やレベルアップ、アイテム獲得などがありますが今日のところは割愛します。

  • ドラッグ(スワイプ)してラケットを動かす(画面の中ならどこでも可)
  • [START]ボタンを押してゲーム開始
  • ボールは一つ
  • ボールがラケットに当たる位置によって角度が変わる
  • ブロックは 9×4、ボールが当たるとブロックが消えて、ボールが跳ね返る
  • ブロックが全部崩れたらクリアでゲーム終了
  • ボールが画面下に落ちたら失敗でゲーム終了
  • タイトル画面でアニメーションを動かす

メインタイムライン

【今回、Animate CC 2019 にバージョンアップしたらユーザーインタフェイスが少し変わりました】

シーンはおおまかに3つ

今回はゲームなので、タイトルパート/ゲームパート/結果パートとシーンを分けて考えていきましょう。

コードはほとんど2フレーム目の title_set と16フレーム目の play_set の2ヶ所に書いてあります。

ゲームを作る場合、ユーザー操作で動く部分(今回ではtitleとplay)はフレームを移動しないでプログラムで動きを作ることが多いです。

クリア失敗(miss)とクリア成功(clear)にフレームを多く使っているのは文字の点滅などをフレームアニメーションで行っているためです。

 

ラケットをドラッグ(pressmove)で動かす

2フレーム目“title_set”のコードです。

//スマホ対応
if (createjs.Touch.isSupported()) {
	createjs.Touch.enable(stage, true);
}

//フレーム移動
this.gotoAndStop("title"); //-- タイトルへ

//インスタンスの変数
var _this = this;
var _area = this.area; //------------------------ プレイエリア
_area.width = _area.nominalBounds.width; //   幅(340)
_area.height = _area.nominalBounds.height; // 高さ(340)
var _racket = this.racket; //-------------------- ラケット
_racket.width = _racket.nominalBounds.width; //   幅(60)
_racket.height = _racket.nominalBounds.height; // 高さ(11)
var _ball = this.ball; //------------------------ ボール
onBallOnRacket(); //ボールをラケットの上に

//リスナー削除(ゲーム終了して戻った時にイベントリスナーの重複を避ける)
_area.removeAllEventListeners();

//----- スタートボタン
_this.startBtn.addEventListener("click", onStartBtn); //--- スタートボタンDown
function onStartBtn(e) {
	e.remove(); //イベントリスナー 削除
	_area.removeEventListener("pressmove", onBallOnRacket); //--(削除)ラケットにボールを乗せる
	//フレーム移動
	_this.gotoAndPlay("play_set"); //プレイ開始
}

//----- ラケットをドラッグで動かす -----
_area.addEventListener("mousedown", onRacketMouseDown); //-- マウスダウン
_area.addEventListener("pressmove", onRacketPressMove); //-- ドラッグ
_area.addEventListener("pressmove", onBallOnRacket); //-- ラケットにボールを乗せる

//マウスダウン
function onRacketMouseDown(e) {
	_racket.offX = _racket.x - e.localX; //-- ラケットとマウスのオフセット
}
//ドラッグ
function onRacketPressMove(e) {
	var moveX = e.localX + _racket.offX; //-- マウスの座標
	if (moveX < (_racket.width / 2)) {
		moveX = (_racket.width / 2); //----- ラケットが左にはみだしたら止める
	} else if (moveX > _area.width - _racket.width / 2) {
		moveX = _area.width - _racket.width / 2; //-- ラケットが右にはみだしたら止める
	}
	_racket.x = moveX;
}

//ボールをラケットの上に
function onBallOnRacket(e) {
	_ball.x = _racket.x;
	_ball.y = _racket.y - 5; //ボールの初期値、ラケットの上
}

今回はタイトル画面でもラケットが動かせるようにしています。そこでラケットを動かすコードは title_set に書きます。

ドラッグ操作のマウスイベントのリスナーは、ラケット(racket)ではなく、プレイエリア(area)というインスタンスに設置します。ステージ全体を覆う area にイベントリスナーを設置することで画面のどこでドラッグしてもラケットが動かせるようになります。

実際に移動するのはラケットですが、小さなラケットにリスナーを設置してしまうとマウスイベントを捉えられずとても扱いづらくなります。とくにスマホの場合は指で画面を操作するため、プレイ中にラケットが指に隠れて見えなくなってしまいます。これを避けるためにも画面全体でマウスイベントを捉えられるようにしました。

これまでもスマホアプリを作るのに Animate で画面全体を覆うインスタンスをよく使いました。シェイプで四角いムービークリップを作り、全体を覆うように配置してプロパティで透明度(アルファ)を0%にすると、透明なボタンが作れて便利でしたが、HTML5 canvas では透明度0%のインスタンスではマウスイベントをキャッチできません。
そこでアルファを1%(限りなく透明)にして配置する方法があるようです。今回は背景が一色なので不透明のインスタンスを背景もかねて置いています。

インスタンスの幅を知る

ラケットとボールの動く範囲を決めたり、ボールとラケットの当たりを見たりと、なにかとインスタンスの幅と高さは必要です。

インスタンス.nominalBounds.width; //幅
インスタンス.nominalBounds.height; //高さ

HTML5 canvas (CreateJS) ではインスタンスの属性(プロパティ)に width(幅)と height(高さ)がありません。無いものは無いんです…と言われても、ゲームを作る上でなにかと必要な情報です。なんとか色々調べたら使えそうな値がありました。ただしこの nominalBounds で得られるのはインスタンスの元となるムービークリップの大きさなので、インスタンスを都合よく拡大縮小したり動的に子インスタンスを動かした場合、思ったような値が返ってきませんし、この値を変更してもインスタンスの形が変わるわけでもありません。

パパ
パパ

これは驚きました。幅や高さがわからないと辛いですね。いつもゲームを作るとき、はじめは仮の絵で作って動かしながら大きさを調整していくので、画面でのサイズが扱えるプロパティが欲しかったぁ…。

もちろん設計段階で大きさは決めて作って実数を使えばいいんですが、プログラムの中で数字を直接書くと意味がわからなくなって、あとで見たときに混乱するんですよね。

まあ、とりあえず今回はエリアやラケットはシンボルに変化する時の大きさを整えて作って nominalBounds の値を勝手にインスタンスのプロパティ(.widthと.height)として加えてしまいます。

ドラッグ(スワイプ)のアクションを作る

HTML5 Canvas (CreateJS) でドラッグをさせる時、たいてい3つのマウスイベントを使ってプログラムするようです。

一般的には以下のようなコードでドラッグ(スワイプ)操作のコードを書きます。

//マウスダウンのリスナー設置
instance.addEventListener("mousedown", onMouseDown);
function onMouseDown(evt){
	//マウスダウンのリスナーを削除
	instance.removeEventListener("mousedown", onMouseDown);
	//ドラッグ中とドラッグ終了のリスナー設置
	instance.addEventListener("pressmove", onPressMove);
	instance.addEventListener("pressup", onPressUp);
	/*マウスダウンのアクション*/
}
function onPressMove(evt){
	/*ドラッグ中のアクション*/
}
function onPressUp(evt){
	//ドラッグ中とドラッグ終了のリスナー削除
	instance.removeEventListener("pressmove", onPressMove);
	instance.removeEventListener("pressup", onPressUp);
	//マウスダウンのリスナー設置
	instance.addEventListener("mousedown", onMouseDown);
	/*ドラッグ終了のアクション*/
}

しかし今回の“ブロック崩し”では不要なイベントリスナーは省略、かなり簡略化してリスナーの設置削除をできるだけ減らしています。

ドラッグ(スワイプ)アクションの考え方

まずmousedownマウスダウンされた時にラケットとマウス座標のオフセット(差分)を採ります。

そしてpressmoveドラッグ中はマウス座標にオフセットを足した位置にラケット座標をセットすると、マウス操作に平行してラケットを移動させられます。

ムービークリップインスタンスの座標

それではまず、ラケットの座標を確認してみましょう。

インスタンスのよこ座標
instance.x
インスタンスのたて座標
instance.y

これでインスタンスの座標がとれます。さらに…

instance.x150;
instance.y100;

例えばこのように書くと、インスタンスの位置を画面の左から150px、上から100pxにセットできます。この .x.yインスタンスのプロパティ(属性)と言われる値です。プロパティにはプログラムで書き換えることで操作可能な値がいろいろあるんですね。

ということで、racket.x を書き換えてラケットを動きをつけるプログラムができます。

ちょっと驚いたのですが、この座標値が必ずしも Animate でインスタンスを配置したときの座標ではないという現象が起こりました。

Animate『自由変形ツール』を選択した時に変形の中心を表す○が表示されますが、この○を動かしてしまうと、インスタンスのx,y座標が○の座標に変わってしまいました。

Animate CC のインスタンスの中央○は、自由変形ツールでコマンドキーを押しながら○をクリックすると本来の位置にリセットできます。
パパ
パパ

ブロック崩しを作っている途中、なぜかラケットの座標が合わなくてハマりました。こういうことなら先に言って欲しかった

マウスの座標を取る

インスタンス.addEventListener("マウスイベント", ファンクション名);
function ファンクション名(e){
        var マウスX = e.localX;
        var マウスY = e.localY;
}

マウスの座標は、マウスイベントに割り当てた関数(function)引数(ひきすう)で渡されるイベントオブジェクトに .localX.localY として収められています。

上のコードを見てください。2行目のファンクション名(e)と書いた場合の変数 eイベントオブジェクトです。この中にマウスイベントのプロパティが収められていて、マウスの座標は e.localX(X座標)、e.localY(Y座標)として取れます。

※変数名は任意です。「e」とか「evt」とか「event」とかがよく使われているようです。

ちなみに、e.stageXとかstage.mouseXなどもマウス座標関連のプロパティですが、別の値が返ってきます。こちらの値はステージ基準のグローバル座標空間における座標値なんだそうです。これに対して Animate でオーサリングする時に使う座標系はローカル座標空間なんだそうで(グローバルの1≠ローカルの1)のようです。詳しくは別の機会に。

マウスが押されたら(mousedown)オフセットを測定する

//----- ラケットをドラッグで動かす -----
_area.addEventListener("mousedown", onRacketMouseDown); //-- マウスダウン

//マウスダウン
function onRacketMouseDown(e) {
	_racket.offX = _racket.x - e.localX; //-- ラケットとマウスのオフセット
}

ラケットは横方向のみの移動なので、オフセットも必要なのはx方向だけです。
ラケットのインスタンスの勝手プロパティ.offXに、ラケットのx座標からマウスのx座標を引いた値を代入しておきます。

ドラッグされたら(pressmove)ラケットを動かす

基本はラケットのよこ座標 .x プロパティを、マウス座標(e.localX)にオフセット(racket.offX)を足した値に書き換えればラケットを動かせます。ですがそのままだとラケットが画面の外に消えてしまうかもしれません。

そこでエリアの幅とラケットの幅を使って計算!、ラケットが左端または右端からはみ出す値になった時はそれ以上進まないようにしてあげます。

//----- ラケットをドラッグで動かす ----- 
_area.addEventListener("pressmove", onRacketPressMove); //-- ドラッグ

//ドラッグ
function onRacketPressMove(e) {
	var moveX = e.localX + _racket.offX; //-- マウスの座標
	if (moveX < (_racket.width / 2)) {
		moveX = (_racket.width / 2); //----- ラケットが左にはみだしたら止める
	} else if (moveX > _area.width - _racket.width / 2) {
		moveX = _area.width - _racket.width / 2; //-- ラケットが右にはみだしたら止める
	}
	_racket.x = moveX;
}

6行目で、とりあえず計算 e.localX + _racket.offX の結果を変数 moveX に代入しておきます。このままでは小さすぎ大きすぎの値も入ってしまいます。そこで…

if (条件式A) {
        実行アクションA;
}else if (条件式B) {
        実行アクションB;
}

if文です。「もしも〇〇であれば、△△をする。そうではなく●●ならば、▲▲をする」という命令です。
条件式はtrue(はい)かfalse(いいえ)という評価ができる式が入ります。

if文の条件式とは例えば…
        var player = “太郎”;
と書いてから
        if (player) {…
というのはエラーです。playerの答えは“太郎”であって「はい/いいえ」では答えられないですね。
一方、
        if (player == “太郎”) {…
となっていれば実行アクションが処理されます。「playerは太郎である:はい/いいえ」が成立します。

変数moveXに入っている値が…

  • A) 小さすぎて左にはみ出したら、moveXを左端の数値に書き換える。
  • B) 大きすぎて右にはみ出したら、moveXを右端の数値に書き換える。

という処理を行えば、ラケットが画面から消えることなく可動域の中で自由に動かすことができますね。

ボールをラケットの上に乗せて動く

タイトル画面では、まだゲームが始まっていないのでボールをラケットの上に置いたままにしたいと思います。

ラケットが動く度に、ボールのインスタンスの座標も動かせばいいので…

にっちゃん

ラケットをドラッグで動かすプログラムに追加で書けばいいじゃん

と思うかもしれませんがラケットを動かす部分はゲームパートでも流用しますが、ボールを乗せるのはタイトルパートだけなので、切り分けられるようにしたいところです。

そこで、もうひとつドラッグ(pressmove)のリスナーを重ねて設置してしまいましょう。

//----- ラケットをドラッグで動かす -----
_area.addEventListener("pressmove", onBallOnRacket); //-- ラケットにボールを乗せる
 
//ボールをラケットの上に
function onBallOnRacket(e) {
	_ball.x = _racket.x;
	_ball.y = _racket.y - 5; //ボールの初期値、ラケットの上
}
当たり前といえばそうなんですが、実は同一のイベントのリスナーを、同じインスタンスにいくつ設置してもちゃんと動きます。もちろん実行アクションは違うものでも…です。

注意しなければいけないのはイベントリスナー設置のコードがあるフレームに戻るときに、全く同じイベントリスナーが重複して設置されないようにしなければいけません。

スタートボタンを作る

スタートボタンが押されるとタイトルパートからゲームパートに移動します。

この時に不要なイベントリスナーを外します。

  • ボールをラケットの上に乗せて動く”pressmove”のリスナー
  • スタートボタンの”click”のリスナー

イベントリスナー を外す3つの方法

  • 特定のイベントリスナー を外す
    インスタンス.removeEventListener(“イベント”, ファンクション名);
  • インスタンスに設置したリスナーを全部外す
    インスタンス.removeAllEventListeners();
  • リスナーで呼び出した関数でリスナーを外す
    function hogehoge(e) {
            e.remove();
    }

イベントリスナー を付けたり外したりしてると、だんだんこんがらがって何がついてて何を外したか訳がわからなくなってきます。しかし、うっかり外し忘れてしまうと「なんでこうなる?」というバグになりやすいのでちゃんと把握して設置・削除を行うようにしましょう

次はいよいよ「ゲームパート」

では続いてブロック崩しのゲーム部分を作りますよ。