ロジック
src/model
にgame
ディレクトリを作り、そこにLogic.js
を作成します。
ロジックの役割
- マウス/タップでの移動処理
- 同じ色のブロックか判定
- 数字の加算と得点の加算処理
- タイムバーの回復
Logic.js
import { RedButtonContent } from "../../content/RedButtonContent";
import { GreenButtonContent } from "../../content/GreenButtonContent";
import { BlueButtonContent } from "../../content/BlueButtonContent";
import { YellowButtonContent } from "../../content/YellowButtonContent";
/**
* @class
* @extends {next2d.fw.Model}
*/
export class Logic extends next2d.fw.Model
{
/**
* @param {number} number
* @constructor
* @public
*/
constructor (number)
{
super();
/**
* @description ランダムにボタンを生成する為の配列
* @type {array}
* @public
*/
this.pieceList = [
RedButtonContent,
GreenButtonContent,
BlueButtonContent,
YellowButtonContent
];
/**
* @description スコア
* @type {number}
* @default 0
* @public
*/
this.score = 0;
/**
* @description 加点対象の倍数値
* @type {number}
* @public
*/
this.number = number | 0;
/**
* @description タイムバーの減算数値
* @type {number}
* @default 0
* @public
*/
this.subNumber = 0;
/**
* @description タイムバーの固定x座標
* @type {number}
* @default 0
* @public
*/
this.maxX = 0;
/**
* @description 移動の対象となるDisplayObject
* @type {MovieClip}
* @default null
* @public
*/
this.moveDisplayObject = null;
/**
* @description ピースの移動を制御
* @type {boolean}
* @default false
* @public
*/
this.lock = false;
}
/**
* @param {next2d.display.MovieClip} piece
* @return {void}
* @public
*/
addButtonEvent (piece)
{
const { MouseEvent } = next2d.events;
piece.buttonMode = true;
piece.mouseChildren = false;
piece.addEventListener(MouseEvent.MOUSE_DOWN, (event) =>
{
const target = event.currentTarget;
this.moveDisplayObject = target;
this.lock = false;
const parent = target.parent;
parent.setChildIndex(target, parent.numChildren - 1);
});
piece.addEventListener(MouseEvent.MOUSE_UP, () =>
{
this.lock = false;
this.moveDisplayObject = null;
});
piece.addEventListener(MouseEvent.ROLL_OVER, (event) =>
{
const target = event.currentTarget;
if (this.moveDisplayObject
&& !this.lock
&& (!target.alpha
|| this.moveDisplayObject.constructor === target.constructor)
) {
switch (true) {
case this.moveDisplayObject.x === target.x && this.moveDisplayObject.y !== target.y:
case this.moveDisplayObject.y === target.y && this.moveDisplayObject.x !== target.x:
{
const { Tween } = next2d.ui;
const { Event } = next2d.events;
this.lock = true;
if (target.alpha) {
const job = Tween.add(this.moveDisplayObject,
{ "x": this.moveDisplayObject.x, "y": this.moveDisplayObject.y },
{ "x": target.x, "y": target.y },
0, 0.05
);
job.addEventListener(Event.COMPLETE, this.movePiece.bind({
"scope": this,
"moveDisplayObject": this.moveDisplayObject,
"target": target
}));
// 新しいピースを追加
const parent = this.moveDisplayObject.parent;
const piece = parent
.addChildAt(this.createPiece(), parent.numChildren);
piece.x = this.moveDisplayObject.x;
piece.y = this.moveDisplayObject.y;
job.start();
} else {
const job = Tween.add(this.moveDisplayObject,
{ "x": this.moveDisplayObject.x, "y": this.moveDisplayObject.y },
{ "x": target.x, "y": target.y },
0, 0.05
);
job.addEventListener(Event.COMPLETE, () =>
{
this.lock = false;
});
target.x = this.moveDisplayObject.x;
target.y = this.moveDisplayObject.y;
job.start();
}
// reset
this.moveDisplayObject = null;
}
break;
default:
break;
}
} else {
this.lock = true;
}
});
}
/**
* @return {void}
* @public
*/
movePiece ()
{
const scope = this.scope;
const target = this.target;
const baseNumber = this.moveDisplayObject.number.text | 0;
const targetNumber = target.number.text | 0;
const afterNumber = baseNumber + targetNumber;
const parent = this.moveDisplayObject.parent;
this.moveDisplayObject.number.text = `${afterNumber}`;
if (afterNumber % scope.number === 0) {
const { Tween } = next2d.ui;
const { Point } = next2d.geom;
const { Event } = next2d.events;
const view = scope.context.view;
// タイマーの回復
scope.recoveryTimeBar(afterNumber / scope.number);
const point = parent.globalToLocal(new Point(
view.score.x,
view.score.y
));
const job = Tween.add(
this.moveDisplayObject,
{
"x": this.moveDisplayObject.x,
"y": this.moveDisplayObject.y,
"alpha": 1
},
{
"x": point.x,
"y": point.y,
"alpha": 0
},
0, 0.25
);
job.addEventListener(Event.COMPLETE, scope.pointUp.bind({
"scope": scope,
"afterNumber": afterNumber,
"target": target,
"moveDisplayObject": this.moveDisplayObject
}));
job.start();
} else {
this.lock = false;
}
parent.removeChild(target);
}
/**
* @return {void}
* @public
*/
pointUp ()
{
const scope = this.scope;
const afterNumber = this.afterNumber;
const target = this.target;
const parent = this.moveDisplayObject.parent;
const piece = parent
.addChildAt(scope.createPiece(), parent.numChildren);
piece.x = target.x;
piece.y = target.y;
scope.score += afterNumber + afterNumber / scope.number;
scope.context.view.score.text = `${scope.score}`;
parent.removeChild(this.moveDisplayObject);
this.lock = false;
}
/**
* @return {next2d.fw.Content}
* @public
*/
createPiece ()
{
// ランダムに選択
const PieceClass = this.pieceList[Math.floor(Math.random() * 4)];
const piece = new PieceClass();
// ボタンとしてのイベントを追加
this.addButtonEvent(piece);
// パズルの数値をセット
this.setNumber(piece);
return piece;
}
/**
* @param {next2d.display.MovieClip} piece
* @return {void}
* @public
*/
setNumber (piece)
{
piece.number.text = Math.floor(Math.random() * 8) + 1;
}
/**
* @param {TimeBarContent} content
* @return {void}
* @public
*/
startTimer (content)
{
const width = content.bar.width;
this.maxX = content.bar.x;
// 幅をタイムアウト時間で分割する
this.subNumber = width
/ (this.config.game.timeLimit
* (this.query.get("number") | 0));
// タイマースタート
const timerId = setInterval(function ()
{
content.bar.x -= this.subNumber;
if (Math.abs(content.bar.x) > width) {
// ゲーム終了
clearInterval(timerId);
// 結果画面にシーン移動
this.app.gotoView(
`game/result?score=${this.context.view.score.text}`
);
}
}.bind(this), 1000);
}
/**
* @param {Number} number
* @return {void}
* @public
*/
recoveryTimeBar (number)
{
const bar = this.context.view.timer.bar;
bar.x += this.subNumber * number;
bar.x = Math.min(bar.x, this.maxX);
}
/**
* @param {next2d.display.MovieClip} piece
* @return {void}
* @public
*/
setLastPiece (piece)
{
piece.alpha = 0;
piece.buttonMode = false;
// remove event
piece.removeAllEventListener(MouseEvent.MOUSE_DOWN);
piece.removeAllEventListener(MouseEvent.MOUSE_UP);
}
}
GamePlayViewModel
昨日の記事で、GamePlayViewModel
にいくつかロジックが入っていましたが、Logic.js
に移行できるものは移行します。
import { TimeBarContent } from "../../content/TimeBarContent";
import { Logic } from "../../model/game/Logic";
/**
* @class
* @extends {next2d.fw.ViewModel}
*/
export class GamePlayViewModel extends next2d.fw.ViewModel
{
/**
* @param {next2d.fw.View} view
* @constructor
* @public
*/
constructor (view)
{
super(view);
// ロジッククラスを生成
this.logic = new Logic(this.query.get("number"));
}
/**
* @param {next2d.fw.View} view
* @return {Promise|void}
* @public
*/
bind (view)
{
return new Promise((resolve) =>
{
const { Sprite } = next2d.display;
const sprite = view.addChild(new Sprite());
let positionX = 0;
let positionY = 0;
// パズルのピースを生成
const pieceCount = this.config.game.piece;
for (let idx = 0; idx < pieceCount; ++idx) {
for (let idx = 0; idx < pieceCount; ++idx) {
const piece = sprite.addChild(this.logic.createPiece());
piece.x = positionX;
piece.y = positionY;
positionX += piece.width + 10;
}
positionX = 0;
positionY += 125;
}
// 最後のピースは移動スペースなので非表示
this.logic.setLastPiece(
sprite.getChildAt(sprite.numChildren - 1)
);
// 枠に収まるサイズに縮小して中心にセット
sprite.scaleX = sprite.scaleY = 0.4;
sprite.x = (this.config.stage.width - sprite.width) / 2;
sprite.y = (this.config.stage.height - sprite.height) / 2;
resolve();
})
.then(() =>
{
return new Promise((resolve) =>
{
// スコア数値
const {
TextField,
TextFieldAutoSize,
TextFormat
} = next2d.text;
const textFormat = new TextFormat();
textFormat.font = "Arial";
textFormat.size = 40;
textFormat.bold = true;
textFormat.color = "#ffffff";
const textField = new TextField();
textField.defaultTextFormat = textFormat;
textField.name = "score";
textField.autoSize = TextFieldAutoSize.CENTER;
textField.thickness = 3;
textField.text = "0";
textField.x = (this.config.stage.width - textField.width) / 2;
textField.y = 40;
view.addChild(textField);
resolve();
});
})
.then(() =>
{
return new Promise((resolve) =>
{
// タイムバー
const content = view.addChild(new TimeBarContent());
content.name = "timer";
content.scaleX = content.scaleY = 0.8;
content.x = (this.config.stage.width - content.width) / 2;
content.y = 110;
this.logic.startTimer(content);
resolve();
});
});
}
}
少しだけ、スッキリしたかと思います。
テスト
jestを利用して、テストケースを実装していきます。
jest.config.js
jest.config.js
を追加します。
module.exports = {
"setupFilesAfterEnv": ["./jest.setup.js"]
};
jest.setup.js
configファイルで読み込む、モック用のjest.setup.js
を作成します。
テストケースで必要なモックをここで作成しておきます。
global.next2d = {
"fw": {
"Content": class Content
{
get app ()
{
return null;
}
},
"Model": class Model
{
get app ()
{
return null;
}
}
}
};
global.location = {
"pathname": "/"
};
LogicTest.js
今回はデフォルトの設定値が正しいかだけのテストケースを記載します。
(後日、テストケースを増やせればと思います。)
import { Logic } from "../../../src/model/game/Logic";
describe("LogicTest initialize test", () =>
{
test("params default value", () => {
const logic = new Logic(10);
expect(logic.pieceList.length).toBe(4);
expect(logic.score).toBe(0);
expect(logic.number).toBe(10);
expect(logic.subNumber).toBe(0);
expect(logic.maxX).toBe(0);
expect(logic.lock).toBe(false);
expect(logic.moveDisplayObject).toBe(null);
});
});
見た目を整える
今回はデザインがない状態からのゲーム制作だったので、ある程度、色味を整えるだけに納めたいと思います。
(デザインのセンスはないので、悪しからず・・・)
GitHub
今回作成したゲームはGitHubに公開してありますので、興味があれば是非ご覧下さい。
GitHub
ゲームはこちらからプレイできます。
Number Puzzle
最後に
Next2Dは素材やアニメーション制作に特化したNext2D NoCode Tool、プログラミングに特化したNext2D Framework
、そして、それらをWebで公開する為のNext2D Player
で構成されています。
また、Flashで制作したSWF資産を再利用する事が可能です。ActionScriptで作り込んだロジックはJavaScriptに置き換える必要がありますが、将来性を考えるとJavaScriptに置き換える事への投資は無駄にならないと思っています。
Amplify
やFirebase
などのBaasサービスを利用する事で、インフラやバックエンドの専門知識がなくても、通信ゲームなどの複雑なゲーム制作も可能になると思うので、今後チャレンジしていきたいと思います。
最後まで記事を読んで頂き、ありがとうございました。