モーリーのメモ

アプリ開発等(プログラミング、CG作成)、興味を持ったことを実践してまとめるブログです。

モーリーのメモ

倉庫番のようなテキストゲームを作る:Cocos2d-x v3.9(JavaScript)

 パソコンのゲームプログラミング入門の『まずはコマンドラインのテキスト表示と、キーボードの操作だけでゲームを作ってみよう!』的なお題をやってみました。
 見た目はかなりショボいですが、倉庫番のような動きを実装しています。
 f:id:mmorley:20160328030436p:plain
 キー入力がないので、そこは以前作成した仮想ジョイパッドを使用していますが、ゲームのメインの表示はラベル一つだけです。
 グラフィックは文字列の操作だけですが、それなりに仕組みを考える必要があります。

使用環境

私が使用している環境です。

作成する内容

実装内容

  • 文字の意味は下記のとおりです。
    • P:プレイヤー
    • B:ブロック
    • G:ゴール
    • W:壁
  • プレイヤーをジョイスティックで1マスずつ移動させる
  • プレイヤーでブロックを押して、ブロックをゴールの上に運ぶ
  • すべてのブロックをゴールの上に運んだら終了
  • プレイヤーとブロックは壁の上には移動できない
  • プレイヤーはゴール上に移動できない
  • プレイヤーはブロックを2個同時に押せない
  • ボタンを押したら、ステージがスタート時に戻る

作成したプログラムのデモ

 下記のリンクから動作を確認できます。

 デモは、『Cocos Studio 2』で『HTML5』パッケージを作成して『GitHub Page』にアップしたものです。
  アップする方法は、下記の記事を参考にして下さい。

作成手順(Cocos Studio 2)

1.使用するデータを準備

  1. 下記のリンク先のページ内の『Raw』を右クリックして、『リンク先を別名で保存』をクリック
    https://github.com/githubmorley/blog-files/blob/master/160327/resources.zip
  2. ダウンロードした『resources.zip』をダブルクリックで解凍

『resources』フォルダには、下記のファイルが含まれています。

  • ビットマップフォント
    • fontGame.fnt
    • fontGame.png
  • 仮想ジョイパッド用画像
    • joystick_background.png
    • joystick_thumb.png
    • button_normal.png
    • button_pressed.png
    • button_disabled.png
  • 仮想ジョイパッドのソースファイル
    • SneakyButton.js
    • SneakyButtonSkinnedBase.js
    • SneakyJoystick.js
    • SneakyJoystickSkinnedBase.js
  • 今回作成するコードの完成版
    • project.json
    • app.js
    • resource.js

2.Cocos Studio 2を起動

  1. 『/Applications/Cocos/Cocos Studio 2』を起動

3.新規プロジェクトを作成

  1. アプリケーションメニューの『File』-『New Project...』をクリック
    f:id:mmorley:20160309120815p:plain:w200
  2. 開いたウィンドウで『All』の『Cocos Project』を選択し、『Next』をクリック
    f:id:mmorley:20160309171301p:plain:w300
  3. 次のように設定を行った後、『Finished』をクリック
    • 『Project Name(プロジェクト名)』:HelloCocos_Game(任意の名前)
    • 『Project Path(プロジェクトの保存先)』:(任意のフォルダ)
    • 『Orientation(画面の向き)』:『縦向きの画面のアイコン』を選択
    • 『Engine Version(Cocos2d-xのバージョン)』:『cocos2d-x-3.9』を選択
    • 『Project Language(使用するプログラミング言語)』:『JavaScript』を選択
      f:id:mmorley:20160323235348p:plain:w300

4.データをインポート

  1. 画面左下の『Resources』ウインドウ内で右クリックし、『Import Resources...』をクリック
    f:id:mmorley:20160328010137p:plain:w200
  2. 手順1で準備したファイルのうち下記のものを選択し、『Open』をクリック
  • ビットマップフォント
    • fontGame.fnt
    • fontGame.png
  • 仮想ジョイパッド用画像
    • joystick_background.png
    • joystick_thumb.png
    • button_normal.png
    • button_pressed.png
    • button_disabled.png

5.スプライトシートの作成

  1. アプリケーションメニューの『File』-『New File...』をクリック
  2. 開いたウインドウで下記のように設定し、『New』をクリック
    • 『Type』:SpriteSheet
    • 『Name』:Plist
      f:id:mmorley:20160311115332p:plain:w300
  3. 『Resources』ウインドウで下記の画像を選択し、『Canvas(画面作成等の作業をするウインドウ)』の『Plist.csi』にドラッグ&ドロップ
    f:id:mmorley:20160328011418p:plain:w300

6.デフォルトの背景画像を削除

  1. Canvas』の『Default(背景のSprite)』をクリックし、キーボードの『delete』を押下して削除
  2. 『Resources』ウインドウの『HelloWorld.png』を右クリックし、『Delete』をクリック
  3. 開いたウインドウで『Move To Trash』をクリック

7.テキスト表示用のBitmapLabelを配置

  1. 『Objects』ウインドウの『Widgets』の『BitmapLabel』を『Canvas』にドラッグ&ドロップ
  2. 『Properties』ウインドウで『BitmapLabel』のプロパティを下記のように変更
    *書いていないパラメータは変更していません。
    グループプロパティ名
    -NameBitmapFontLabel
    Position & SizeAnchor PointX 0.5、Y 1
    PositionX 320、Y 800
    FeatureFont FilefontGame.fnt(『Resources』ウインドウ
    からドラッグ&ドロップして登録)

8.保存

これまでの作業を保存します。

  1. アプリケーションメニューの『File』-『Save All』をクリック

9.プロジェクトのパブリッシュ

  1. アプリケーションメニューの『Project』-『Publish and Package...』をクリック
  2. 開いたウインドウで下記のように設定し、『OK』をクリック
    • 『Publish』の『Publish Type』の『Publish to Code IDE』を選択
      f:id:mmorley:20160315180529p:plain:w200

『Cocos Code IDE』が起動し、プロジェクトが読み込まれます。

作成手順(Cocos Code IDE

1.仮想ジョイパッドのソースファイルを追加

  1. 『Cocos Code IDE』の画面左の『Explorer』で、『srcフォルダ』を右クリックし、『インポート』をクリック
  2. 開いたウインドウで、『一般』-『ファイルシステム』をクリックし、『次へ』をクリック
    f:id:mmorley:20160328023544p:plain:w300
  3. 次のウインドウで、『参照』をクリックし、手順1の『resources』フォルダを選択
  4. 下記のファイルにチェックを入れた後、『終了』をクリック
    f:id:mmorley:20160328023843p:plain:w300

2.『project.json』に仮想ジョイパッドのソースファイルを登録

  1. 『Cocos Code IDE』の画面左の『Explorer』で、プロジェクトフォルダ直下の『project.json』をダブルクリックして開く
  2. 『project.json』の『"jsList" : []』を下記のように編集
    "jsList" : [
        "src/SneakyButtonSkinnedBase.js",
        "src/SneakyButton.js",
        "src/SneakyJoystickSkinnedBase.js",
        "src/SneakyJoystick.js",
        "src/resource.js",
        "src/app.js"
    ]

3.『resource.js』にリソースファイルを登録

  1. Explorer』で、『srcフォルダ』にある『resource.js』をダブルクリックして開く
  2. 『resource.js』の『var res = {}』を下記のように編集
var res = {
		MainScene_json : "res/MainScene.json",
		Plist_plist : "res/Plist.plist", // スプライトシートのplistファイルを追加
		Plist_png : "res/Plist.png" // スプライトシートのpngファイルを追加
};

4.『app.js』の編集

 メインとなるソースコードです。

  1. Explorer』で、『srcフォルダ』にある『app.js』をダブルクリックして開く
  2. 『app.js』を下記のように編集
// グローバル変数(定数扱い)
var gStageWidthLF = 9; // ステージの幅
var gStageWidth = 8; // ステージの幅
var gStageHeight = 5; // ステージの高さ
var gInputInterval = 60 * 0.5;
var gStageString =
	"WWWWWWWW\n" +
	"W GGP  W\n" +
	"W BB   W\n" +
	"W      W\n" +
	"WWWWWWWW\n";

//列挙型
if(typeof eTypeChar == "undefined") { // テキストの種類
	var eTypeChar = {};
	eTypeChar.Player = "P"; // プレイヤー
	eTypeChar.Goal = "G"; // ゴール
	eTypeChar.Wall = "W"; // 壁
	eTypeChar.Space = " "; // スペース
	eTypeChar.Block = "B"; // ブロック
};

if(typeof eMainSeq == "undefined") { // メインシーケンス
	var eMainSeq = {};
	eMainSeq.Init = 1; // 初期化
	eMainSeq.Input = 2; // 入力待ち
	eMainSeq.Interval = 3; // 入力間隔
	eMainSeq.Wait = 4; // 入力待ち
};

var HelloWorldLayer = cc.Layer.extend({  // cc.Layerを継承
	joystick_:null, // ジョイスティック
	button_:null, // ボタン
	bmLabel:null, // ラベル
	arStageChar:[], // ステージの配列
	playerPos:cc.p(0, 0), // プレイヤーの座標
	playerIndex:0, // プレイヤーのインデックス
	mainSeq:0, // メインシーケンス
	countInterval:0, // 入力間隔
	goalNum:0, // 残りゴールの数
    ctor:function () { // コンストラクタ
    	this._super(); // 親クラスのメソッドを継承
    	cc.spriteFrameCache.addSpriteFrames(res.Plist_plist); // スプライトシートをキャッシュに登録
    	var mainscene = ccs.load(res.MainScene_json); // MainSceneのJSONファイルを読み込む
        this.addChild(mainscene.node); // レイヤーに追加
        this.createJoyPad(); // ジョイパッドを作成
        this.bmLabel = mainscene.node.getChildByName("BitmapFontLabel"); // BitmapFontLabelを取得
        this.scheduleUpdate(); // メインループを開始
        this.mainSeq = eMainSeq.Init; // 初期化処理
        return true;
    },
    onExit:function(){ // 親クラスのメソッドを継承
    	this._super();
    	cc.spriteFrameCache.removeSpriteFramesFromFile(res.Plist_plist); // キャッシュからスプライトシートを削除
    },
    update:function(dt){ // メインループ
    	switch (this.mainSeq) { // メインシーケンスごとの処理
    	case eMainSeq.Init: // 初期化処理
    		this.arStageChar = []; // 配列を初期化
    		this.goalNum = 0; // 残りゴール数を初期化
    		for (var int = 0; int < gStageString.length; int++) { // ステージの文字数分ループ
    			var c = gStageString.substr(int, 1); // 一文字取得
    			this.arStageChar.push(c);	// 1文字ごと配列に格納
    			switch (c) { // 文字をチェック
    			case eTypeChar.Player: // プレイヤーの場合
    				this.playerPos = cc.p(int % gStageWidthLF, Math.floor(int / gStageWidthLF)); // 座標を取得
    				this.playerIndex = int; // インデックスを取得
    				break;
    			case eTypeChar.Goal: // ゴールの場合
    				this.goalNum ++; // 残りゴールの数をカウント
    				break;
    			}
    		}
    		this.bmLabel.setString(gStageString); // ラベルにステージデータを表示
    		this.mainSeq = eMainSeq.Input; // 入力処理へ移行
    		break;
		case eMainSeq.Input: // 入力処理
			if ( this.button_.getValue() == 1 ) { // ボタンが押された場合
				this.mainSeq = eMainSeq.Init; // ゲームをやり直す
			}
			// ↓参照渡しにならないように値を取得
			var velocity = cc.p(this.joystick_.getVelocity().x, this.joystick_.getVelocity().y); // ジョイスティックの値を取得
			// ジョイスティックは4方向のみ動きます。
			// 戻り値は、上=(0, 1)、下=(0, -1)、右=(1, 0)、左=(-1, 0)、入力無=(0, 0)です。
			velocity.x = Math.round(velocity.x); // 0/1の値にする
			velocity.y = -Math.round(velocity.y); // 0/1の値にし、座標を反転
			if (velocity.x + velocity.y == 0) break; // 入力がない場合処理を抜ける
			var newPos = cc.pAdd(this.playerPos, velocity); // 移動先の座標を計算
			if(newPos.x < 0 || newPos.x >= gStageWidth
					|| newPos.y < 0 || newPos.y >= gStageHeight) break; // 移動先がステージ外の場合処理を抜ける
			
			// 移動処理
			var newIndex = newPos.y * gStageWidthLF + newPos.x; //移動先のインデックスを計算
			switch (this.arStageChar[newIndex]) { // プレイヤーの移動先をチェック
			case eTypeChar.Space: // スペースの場合
				this.arStageChar[this.playerIndex] = eTypeChar.Space; // 移動元をスペースに変更
				this.arStageChar[newIndex] = eTypeChar.Player; // 移動先をプレイヤーに変更
				this.playerPos = newPos; // プレイヤーの座標を更新
				this.playerIndex = newIndex; // プレイヤーのインデックスを更新
				break;
			case eTypeChar.Wall: // 壁の場合、移動しない
				break;
			case eTypeChar.Block: // ブロックの場合
				var newPos2 = cc.pAdd(newPos, velocity); // 2マス先の座標を計算
				if(newPos2.x < 0 || newPos2.x >= gStageWidth
						|| newPos2.y < 0 || newPos2.y >= gStageHeight) break; // 2マス先がステージ外の場合処理を抜ける
				var newIndex2 = newPos2.y * gStageWidthLF + newPos2.x; //2マス先のインデックスを計算
				switch (this.arStageChar[newIndex2]) { // ブロックの移動先をチェック) {
				case eTypeChar.Goal: // ゴールの場合
					this.goalNum --; // ゴール数を減らす(breakせずスペースの場合と共通処理)
				case eTypeChar.Space: // スペースの場合
					this.arStageChar[this.playerIndex] = eTypeChar.Space; // 移動元をスペースに変更
					this.arStageChar[newIndex] = eTypeChar.Player; // プレイヤーの移動先をプレイヤーに変更
					this.arStageChar[newIndex2] = eTypeChar.Block; // ブロックの移動先をブロックに変更
					this.playerPos = newPos; // プレイヤーの座標を更新
					this.playerIndex = newIndex; // プレイヤーのインデックスを更新
					break;
				}
				break;
			}

			// ステージを描画
			var stageString = ""; // ステージの文字列取得用
			for (var int = 0; int < this.arStageChar.length; int++) { // 文字数分ループ
				stageString += this.arStageChar[int]; // 配列から文字列に追加
			}
			if (this.goalNum == 0) { // 残りゴール数が0の場合
				stageString += "COMPLATE"; // クリア表示
				// 入力の間を開ける
				this.mainSeq = eMainSeq.Wait; // ボタンに入力待ち
			} else {
				// 入力の間を開ける
				this.mainSeq = eMainSeq.Interval; // 入力間隔処理
				this.countInterval = gInputInterval; // カウント数をセット
			}
			this.bmLabel.setString(stageString); // ラベルに表示
			break;
		case eMainSeq.Interval: // 入力間隔処理(ジョイスティックを倒し続けた場合に移動に間隔を開ける)
			if (-- this.countInterval < 1) { // カウントが完了した場合
				this.mainSeq = eMainSeq.Input; // 入力処理に移行
			} else { // カウント中
				var velocity = this.joystick_.getVelocity(); // ジョイスティックの値を取得
				if (velocity.x + velocity.y == 0){ // ジョイスティックが開放されたら
					this.mainSeq = eMainSeq.Input; // 入力処理に移行
				}
			}
			break;
		case eMainSeq.Wait: // ボタン入力待ち
			if ( this.button_.getValue() == 1 ) { // ボタンが押された場合
				this.mainSeq = eMainSeq.Init; // ゲームをやり直す
			}
		}
    },
    createJoyPad:function(){ // ジョイスティックを作成
    	var joystickSkin = new SneakyJoystickSkinnedBase(); // ジョイスティックスキンのインスタンスを作成
    	joystickSkin.setPosition(160, 120); // ジョイスティックスキンを配置
    	joystickSkin.setBackgroundSprite( // 背景部分のスプライトを設定
    			cc.Sprite(cc.spriteFrameCache.getSpriteFrame("joystick_background.png"))); // スプライトシートから読み込み
    	joystickSkin.setThumbSprite( // 親指部分のスプライトを設定
    			cc.Sprite(cc.spriteFrameCache.getSpriteFrame("joystick_thumb.png"))); // スプライトシートから読み込み
    	var joystick = new SneakyJoystick(cc.rect(0, 0, 128, 128)); // ジョイスティックのインスタンスを作成
    	joystick.setIsDPad(true); // デジタルジョイパッドを有効
    	joystick.setNumberOfDirections(4); // デジタルジョイパッドの方向数を設定
    	joystick.setHasDeadzone(true); // デッドゾーンを有効
    	joystick.setDeadRadius(32); // デッドゾーンの半径を設定
    	joystick.setAutoCenter(true); // 自動センター復帰機能を有効
    	joystickSkin.setJoystick(joystick); // ジョイスティックスキンにジョイスティックのインスタンスを渡す
    	this.addChild(joystickSkin); // 自ノードにジョイスティックスキンを追加
    	this.joystick_ = joystick; // ジョイスティックを取得
    	
    	var buttonSkin = new SneakyButtonSkinnedBase(); // ボタンスキンのインスタンスを作成
    	buttonSkin.setPosition(480, 120); // ボタンスキンを配置
    	buttonSkin.setDefaultSprite( // デフォルト状態のスプライトを設定
    			cc.Sprite(cc.spriteFrameCache.getSpriteFrame("button_normal.png"))); // スプライトシートから読み込み
    	buttonSkin.setPressSprite( // 押下状態のスプライトを設定
    			cc.Sprite(cc.spriteFrameCache.getSpriteFrame("button_pressed.png"))); // スプライトシートから読み込み
    	buttonSkin.setActivatedSprite( // 処理中状態のスプライトを設定
    			cc.Sprite(cc.spriteFrameCache.getSpriteFrame("button_disabled.png"))); // スプライトシートから読み込み
    	var button = new SneakyButton(cc.rect(0, 0, 64, 64)); // ボタンのインスタンスを作成
    	button.setIsHoldable(false); // ホールド機能を無効
    	button.setRateLimit(1 / 120); // ホールド機能無効時のボタンON時間を設定
    	button.setIsToggleable(false); // トグル機能を無効
    	buttonSkin.setButton(button); // ボタンスキンにボタンのインスタンスを渡す
    	this.addChild(buttonSkin); // 自ノードにボタンを追加
    	this.button_ = button; // ボタンを取得
    }
});

var HelloWorldScene = cc.Scene.extend({
    onEnter:function () {
        this._super();
        var layer = new HelloWorldLayer();
        this.addChild(layer);
    }
});

あとがき

 1文字ごとの幅を揃えるために、ビットマップフォントを利用しました。