マリオのノコノコを実装する

ノコノコの実装(プログラミングでマリオを作る第40回)

前回から約2ヶ月ほど空いてしまいましたが、
今回はめんどくさい処理の一つノコノコの実装を行います。

まず、今回の実装内容のgifをご覧ください。

マリオのノコノコを実装する

gifでは、甲羅状態から通常状態へ戻る処理と、ノコノコ攻撃時のキノコボックスとの当たり判定
ブロックとの当たり判定、敵との当たり判定の処理を確認できるようにしています。

実装内容まとめ

今回実装する処理をまとめます。

一度踏みつけると甲羅状態になり、再度ふれると、攻撃状態になり、高速で移動する

攻撃状態では、破壊ブロックに触れるとブロックを破壊し、敵も倒すことができる

甲羅状態からしばらく待つと、復帰アニメーションを行い、通常時に戻る

その他は大体クリボと同じです。

新しく実装する内容

今回ノコノコの高さのサイズを通常時で20Pixcel攻撃・待機時に18Pixcelにしたため、
マリオが踏みつけた時にすり抜けてしまい正しく判定されないことがあります。

なので、マリオのY軸方向の移動量を分割して当たり判定を行うことによって、
すり抜けを回避できるように実装します。

では、早速実装していきます。

ノコノコの画像を書く

ノコノコっぽい敵キャラの画像を新しく描いたので、ダウンロードしてResourceフォルダの中に入れてください。

マリオノコノコの画像

上段の画像が、歩いている状態で、中断が甲羅の中に入っている状態で、
下段が甲羅の中にいる状態から歩行状態へ移行する時の画像になります。

上段のノコノコの高さが20、甲羅状態時は18になっています。

では、プログラムを書いていきます。

ノコノコクラスを作成する

クリボの時と同様にノコノコクラスを作ります。

名前をnokonoko.jsにしました。

基本的にはクリボと同じなので、クリボクラスに処理を付け加える感じになります。

ノコノコクラスのコンストラクタ

ノコノコクラスのメンバー変数を定義します。

新しく追加するものに、関してはコメントを振ってあります。

function Noko(posX,posY,dir){
	// 定数
	this.AWAKING_CNT = 180;		// 60フレーム1秒計算
	this.AWAKE_CNT =  300;
	this.NORMAL_HEIGHT = 20;
	this.ATTACK_HEIGHT = 18;
	// 変数
	this.posX = posX;
	this.posY = posY;
	this.addPosX = 0;
	this.addPosY = 0;
	this.animCnt = 0;
	this.animX = 0;
	this.animY = 0;
	this.direction = dir;
	this.rightMapX = 0;
	this.leftMapX = 0;
	this.upMapY = 0;
	this.downMapY = 0;
	// chapter27
	this.state = NORMAL_STATE;
	this.height = this.NORMAL_HEIGHT;
	this.deadCnt = 0;
	// 甲羅状態の移動量
	this.ATTACK_MOVE_X = 7;
	// block破壊用変数
	this.blockAttackX = [[0,0,0,0],[0,0,0,0]];		// Block破壊時の座標X
	this.blockAttackY = [[0,0,0,0],[0,0,0,0]];		// Block破壊時の座標Y
	this.blockAttackCnt = [0,0];			// animation cnt
	this.blockAttackIndex = 0;			// blockのindex
	this.isBlockAttack = [false,false];	// 破壊フラグ
	this.blockAttackAddY = [0,0];			// ブロックの移動量Y
	this.blockAttackIndexX = [0,0];		// ブロックを移動させる対象のブロックマップチップ番号
	this.blockAttackIndexY = [0,0];		// ブロックを移動させる対象のブロックマップチップ番号Y
	// 甲羅状態から復帰するためのアニメーション変数
	this.normalBackCnt = 0;
	// マリオと当たり判定があった場合一度マリオから離れてからでないと当たり判定を起こさせないためのフラグ
	this.isStickyMario = false;
}

ノコノコの描画関数

クリボの時と違い、ノコノコの高さ分だけ画像を切り取って描画しているので、
注意してください。

また、ノコノコが転がっている時には、ブロックを壊すことができるので、
破壊ブロックを描画する関数を呼んでいます。

処理自体はマリオと実装した時と変わりありません。

Noko.prototype.draw = function(ctx,texture,scrollX){
	if(this.state != DEAD){
		ctx.drawImage(texture, (this.animX * 32) + (this.direction * 128),this.animY + (MAP_SIZE - this.height),32,this.height,this.posX - scrollX,this.posY,32,this.height);
	}
	// 甲羅でブロックを破壊した時用のブロックマップチップの描画
	this.drawBlock(ctx,texture,scrollX);
}

ノコノコとマリオとの当たり判定

クリボとは違い、ノコノコには、歩いている状態と甲羅に入って動かない状態、
甲羅での攻撃状態があり、当たり判定の挙動が異なるので、それぞれ処理分岐させる必要があります。

なので、state変数にノコノコの状態を管理し、その値に応じて処理を変えています。

Noko.prototype.collisionWithMario = function(map,mario){
	if(!mario.isDead()){
		// x軸
		if(mario.moveNumX < this.posX + 32 && mario.moveNumX + 32 > this.posX){
			// 当たり判定の範囲が小さいので、2分割して処理する
			var addY = mario.addPosY / 2;
			// 移動する前の座標を保存する
			var marioPosY = mario.posY + mario.addPosY;
			for(var i = 0;i < 2;++i){
				// 移動する量を2分割する
				marioPosY -= addY;
				// マリオの上とノコノコの下(ノコノコはnormalは20攻撃時は18で切り取られる)
				if(marioPosY <= this.posY + this.height){
					// マリオの下がノコノコの上よりも上にある
					if(marioPosY + mario.height >= this.posY){
						// ノコノコの動きを止めるアクション、ノコノコが歩いている時と甲羅移動中の当たり判定はクリボと同じ
						if(this.state == NORMAL_STATE || this.state == NOKO_ATTACK_STATE){
							// マリオの下がノコノコの中間地点よりも上にある
							if(marioPosY + mario.height <= this.posY + (this.height / 2)){
								if(!this.isSticky){
									if(this.height != this.NORMAL_HEIGHT){
										// 縮まるので、高さを変える
										this.height = this.ATTACK_HEIGHT;
										this.posY += this.NORMAL_HEIGHT - this.ATTACK_HEIGHT;										
									}
									// 止まった状態の甲羅にする
									this.state = NOKO_WAIT_STATE;
									// 歩く状態に戻るアニメーションカウントを戻す
									this.normalBackCnt = 0;
									this.animY = 32;
									mario.jumpPower = STEP_UP_NUM;
									this.isSticky = true;
								}
								return;
							}
						 	else{
								if(!this.isSticky){
									mario.collisionWithEnemy(map);
									this.isSticky = true;								
								}
								return;
							}
						}
					
						// 甲羅待機状態の時は、どこに当たっても甲羅を移動させる
						else if(this.state == NOKO_AWAKING_STATE || this.state == NOKO_WAIT_STATE){
							if(!this.isSticky){
								// 甲羅の移動方向を決める
								// 甲羅の中心座標
								var nokoCenterX = this.posX / 2;
								// マリオの中心位置
								var marioCenterX = mario.moveNumX / 2;
								// 甲羅の中心とマリオの中心位置を割り出して、マリオが左側ならば右に、マリオが右側ならば左に移動させる
								this.direction = marioCenterX <= nokoCenterX ? RIGHT_DIR : LEFT_DIR;
								// 甲羅突進状態
								this.state = NOKO_ATTACK_STATE;
								this.isSticky = true;
							}
							return;
						}
					}
				}				
			}
		}
	}
	// 連続してノコノコがマリオに触れている場合は当たり判定を起こさせないためのフラグ
	this.isSticky = false;
}

当たり判定のすり抜け対応

また、冒頭にも書いたように、マリオの下降量が多いと、踏みつけ判定ですり抜けてしまい、
当たり判定になってしまうケースがあるので、マリオの移動量を2分割し、すり抜けを回避しています。

// 当たり判定の範囲が小さいので、2分割して処理する
var addY = mario.addPosY / 2;
// 移動する前の座標を保存する
var marioPosY = mario.posY + mario.addPosY;
	for(var i = 0;i < 2;++i){
	// 移動する量を2分割する
	marioPosY -= addY;

マリオのY軸方向の移動量はaddPosYに保存されているので、現在の座標からaddPosYの座標を引くor足すことで、
1フレーム前の位置を計算することできます。

1フレーム前の位置から、移動量を分割して判定することによって、すり抜けずに判定することができます。

今回は、移動量を2分割していますが、マリオの移動量が多い場合は、さらに分割することにより、
すり抜けを防ぐことができます。

ノコノコの攻撃状態時になった時にマリオも攻撃してしまう状況への対処方法

ノコノコが甲羅状態からマリオと接触して攻撃状態になる際には、マリオは上方向にはねないため、
次のフレームでマリオとノコノコとの当たり判定が起こってしまい、マリオの死亡処理が走ってしまいます。

なので、ノコノコとマリオが一度当たり判定を起こしてから、離れるまでの間は、当たり判定が起こらないように実装しました。

その状態を管理するのに、isStickyというメンバ変数を定義しています。

当たり判定で重要な部分はこんなところでしょうか。

ノコノコの移動処理

続いて、移動処理を実装します。

攻撃時と通常移動じでは、移動速度が違うなど、その辺りの実装が必要ですが、
特に難しいところはありません。

Noko.prototype.move = function(mapChip,moveNum,mario){
	this.updateMapPosition();
	// 通常時と甲羅攻撃時のみ動かす
	if(this.state == NORMAL_STATE || this.state == NOKO_ATTACK_STATE){
		var speed = this.state == NORMAL_STATE ? moveNum : this.ATTACK_MOVE_X;
		// 向きにより加算量を調整する
		moveNum = this.direction == LEFT_DIR ? -speed : speed;
		// 加算量を代入する
		this.addPosX = moveNum;
		// x軸との当たり判定
		this.collisionX(mapChip,this.posX + this.addPosX,mario);
		this.posX += this.addPosX;
		// 移動したのでマップ座標更新
		this.updateMapPositionX(this.posX);
	}
	
	// animation
	if(this.animCnt++ >= 12){
		this.animCnt = 0;
		// 一定以上に達したらアニメーションを更新する
		if(++this.animX > 3){
			this.animX = 0;
		}
	}
	// 甲羅状態から移動状態に戻るための処理
	this.awakeAnimation();
}

甲羅状態から通常移動に戻る際の処理をawakeAnimationという名前の関数で定義しました。

/**
 *	甲羅から歩く動作に復帰するための関数
 */
Noko.prototype.awakeAnimation = function(){
	if(this.state == NOKO_WAIT_STATE){
		this.normalBackCnt++;
		if(this.normalBackCnt >= this.AWAKING_CNT){
			this.state = NOKO_AWAKING_STATE;
			this.animY = 64;
		}
	}
	else if(this.state == NOKO_AWAKING_STATE){
		this.normalBackCnt++;
		if(this.normalBackCnt >= this.AWAKE_CNT){
			this.state = NORMAL_STATE;
			this.height = this.NORMAL_HEIGHT;
			// heightが変わると、めり込むので位置を上げる
			this.posY -= (this.NORMAL_HEIGHT - this.ATTACK_HEIGHT);
			this.normalBackCnt = 0;
			this.animY = 0
		}
	}
}

normalBackCntというカウンター用の変数を定義して、animationの管理をしています。

このゲームは60フレームで動作することを想定しており、
AWAKING_CNT = 180と定義したので、3秒でノコノコがジタバタするアニメーションが始まることになります。

ノコノコと敵との当たり判定

ノコノコが攻撃状態時に敵と当たり判定があった場合、
敵を倒す処理を実装しないといけないので、その処理を書きます。

Noko.prototype.collisionWithEnemy = function(kuribos,nokos){
	// 攻撃状態のみ
	if(this.state == NOKO_ATTACK_STATE){
		// クリボ
		if(kuribos != null){
			for(var i = 0;i < kuribos.length;++i){
				// x軸
				if(kuribos[i].posX < this.posX + 32 && kuribos[i].posX + 32 > this.posX){
					// クリボの上とノコノコの下
					if(kuribos[i].posY <= this.posY + this.height){
						// クリボの下がノコノコの上よりも上にある
						if(kuribos[i].posY + 32 >= this.posY){
							kuribos[i].setDeadCollisionAction();
						}
					}
				}
			}
		}
		// nokonoko自身も渡されるので自身は覗く
		if(nokos != null){
			for(var i = 0;i < nokos.length;++i){
				// ノコノコとの当たり判定があった場合に死亡状態になるので、死亡判定をする必要がある
				if(!this.isDead()){
					// 自身も渡されるので、自身との判定は除く
					if(nokos[i] != this){
						// x軸
						if(nokos[i].posX < this.posX + 32 && nokos[i].posX + 32 > this.posX){
							// ノコノコ上とノコノコの下
							if(nokos[i].posY <= this.posY + this.height){
								// クリボの下がノコノコの上よりも上にある
								if(nokos[i].posY + nokos[i].height >= this.posY){
									// 相手ノコノコが攻撃状態の場合は自身も当たり判定を呼ぶ
									if(nokos[i].state == NOKO_ATTACK_STATE){
										this.setDeadCollisionAction();
									}
									nokos[i].setDeadCollisionAction();
								}
							}
						}
					}
				}
			}		
		}
	}
}

関数の引数nokosには、main.jsで定義したnokonokoを渡すので、
自身も入っています。

なので、このままだと自分と自分のあたり判定も発生してしまうので、
自身を除くための処理を書いています。

// 自身も渡されるので、自身との判定は除く
	if(nokos[i] != this){

また、死亡判定のアニメーションはファイアとの当たり判定時と同じものを利用しています。

攻撃時のノコノコとマップチップオブジェクトとの当たり判定

ノコノコの攻撃時に、ブロックマップチップと当たり判定があった場合は、
ブロックを破壊させ、キノコブロックと当たり判定があった場合は、
キノコを出現させる必要があるので、その処理を書きます。

Noko.prototype.collisionX = function(map,posX,mario){
	this.updateMapPositionX(posX);
    // マップ座標yを配列で保管する
    var mapsY = [this.upMapY,this.downMapY];
    for(var i = 0;i < 2;++i){
    	// ノコノコの右側との当たり判定
    	if(isObjectMap(map[mapsY[i]][this.rightMapX])){
    		// 方向転換を行う
    		// (加算される前の)中心点からの距離を取る
    		var vecX = Math.abs((this.posX + HALF_MAP_SIZE) - ((this.rightMapX * MAP_SIZE) + HALF_MAP_SIZE));
    		this.addPosX = Math.abs(MAP_SIZE - vecX);
    		
    		// 甲羅移動時の判定
    		if(this.state == NOKO_ATTACK_STATE){
        		// 破壊可能なマップチップだった場合
        		if(isBlockMap(map[mapsY[i]][this.rightMapX])){
        			// 破壊アニメーションをさせる
        			this.blockAction(this.rightMapX,mapsY[i],map);
        		}
        		// キノコブロックだった場合
        		else if(isKinokoBlock(map[mapsY[i]][this.rightMapX])){
        			var posX = this.rightMapX * MAP_SIZE;
        			var posY = mapsY[i] * MAP_SIZE;
        			mario.activateKinoko(posX,posY,this.direction);
        			// 空マップにする
	    			replaceEmptyBoxMap(map,this.rightMapX,mapsY[i]);
        		}
    		}
    		this.direction = LEFT_DIR;
    	}
    	// ノコノコの左側
    	else if(isObjectMap(map[mapsY[i]][this.leftMapX]) || isObjectMap(map[mapsY[i]][this.leftMapX])){
    		// (加算される前の)中心点からの距離を取る
    		var vecX = Math.abs((this.posX + HALF_MAP_SIZE) - ((this.leftMapX * MAP_SIZE) + HALF_MAP_SIZE));
    		this.addPosX = -Math.abs(MAP_SIZE - vecX);
			// 甲羅移動の場合
    		if(this.state == NOKO_ATTACK_STATE){
	    		// 破壊可能なマップチップだった場合
	    		if(isBlockMap(map[mapsY[i]][this.leftMapX])){
	    			// 破壊アニメーションをさせる
	    			this.blockAction(this.leftMapX,mapsY[i],map);
	    		}
	    		// キノコブロックだった場合
	    		else if(isKinokoBlock(map[mapsY[i]][this.leftMapX])){
	    			var posX = this.leftMapX * MAP_SIZE;
	    			var posY = mapsY[i] * MAP_SIZE;
	    			mario.activateKinoko(posX,posY);
	    			replaceEmptyBoxMap(map,this.leftMapX,mapsY[i]);
	    		}
    		}
    		this.direction = RIGHT_DIR;
    	}
    }
}

キノコオブジェクトをマリオクラス内に定義しているので、
このオブジェクトを利用します。

マリオクラス内に、キノコを出現させる関数activateKinokoを定義しました。

mario.js

/**
 * chapter40
 * キノコを有効化する関数
 */
Mario.prototype.activateKinoko = function(posX,posY,direction){
	if(this.isBig()){
		this.fireKinoko.activate(posX,posY);
	}else{
		this.kinoko.activate(posX,posY,direction);
	}
}

その他の処理について

実装するの繋ぎこみの部分のコードの解説は省略しているので、
githubの差分を確認してください。

まとめ

絵が必要なのと書くコードが多いので、大変だった。

今後効率的に実装できるように、コード修正したくなってきました。

とりあえず、めんどくさい処理筆頭のノコノコが終わったんですが、
まだ無敵処理と、stage clear処理が残っておる…

次は、無敵処理を実装することになると思います。