ノコノコ連続倒し1up

マリオのスコア処理と連続1up処理(プログラミングでマリオを作る第47回)

前回からさほど開かずに今回は、マリオのスコア処理と連続1up処理の実装をします。

連続1upは2種類の1up方法があったと思います。

一つは連続で敵を踏みつけて1up処理を行うというもの。

マリオ敵の踏みつけによる連続1up処理

もう一つは、ノコノコの甲羅を連続して敵に当てて1up処理を行うものです。

ノコノコの甲羅による、連続1upの実装

では、今回の実装内容を確認します。

今回実装する内容

1.スコアを管理する変数を持って画面にスコア表示させる

2.敵を倒した時にスコアを表示する処理

3.キノコ・コインを取得する際に得点を加算する処理

4.敵を連続で踏みつけた場合に1upさせる処理

5.甲羅で連続して敵を倒した際に1upさせる処理

スコア表示だけでなく、1upまでいっぺんに実装してしまいます。

では、実装していきましょう。

const.js

必要な定数を定義します。

// chapter47
let MAX_SCORE_DIGITS = 6;
let MAX_SCORE = 999999;
let STOMPING_SCORES = [0,100,200,400,800,1000,2000,4000,8000,10000];
let COIN_SCORE = 100;
let KINOKO_SCORE = 1000;
let FIRE_SCORE = 2000;

STOMPING_SCORESに連続踏みつけの際のスコアを定義します。

これが8000を越えたら1upと判定します。

global.js

スコア用の変数をglobal.jsに定義します。

let gScore = 0;

main.js

スコアの表示関数を書いて、main関数でスコア表示関数を呼ぶようにします。

初期のマリオでは、10万点までのスコア表示でした。

/**
 * 10万点まで埋める
 * @param {*} posX 
 * @param {*} posY 
 * @param {*} score 
 */
function drawScore(posX,posY,score){
  let digits = getDigits(score);	// 桁数を取得
  let maxNumber = getMaxNumber(digits);
  // 描画位置
  let numberPosX = posX;
  let zeroNum = MAX_SCORE_DIGITS - digits;
  // 満たない部分は0で描画する
  for(var i = 0;i < zeroNum;++i){
    g_Ctx.drawImage(gCoinTex,0,0,20,17,numberPosX,posY,20,17);
    numberPosX += 25;
  }
  while(maxNumber >= 1){
    // 一番上の桁数から描画する
    g_Ctx.drawImage(gCoinTex, Math.floor((score / maxNumber )) * 20,0,20,17, numberPosX,posY, 20, 17);
    score -= Math.floor((score / maxNumber)) * maxNumber;				// 一番上の桁数を引く(111だったら100を引く)
    maxNumber = Math.floor(maxNumber / 10);		// 111 = 11にする
    numberPosX += 25;
  }
}

敵用のスコア表示処理

まず、クリボにスコア表示処理を実装します。

やられた状態により、スコアが変わるので、スコア用の変数を定義します。

// chapter47
this.score = 100;

次に敵用のスコア描画関数を定義します。

これは、あらゆる敵に共通しているので、大元の関数はutilsファイルに定義します。

死亡判定中にスコア表示を行うようにします。

/**
 * スコアを描画する
 * @param {*} ctx 
 * @param {*} texture 
 * @param {*} oneUpTex 
 * @param {*} scrollX 
 */
Kuribo.prototype.drawScore = function(ctx,texture,oneUpTex,scrollX){
	if(this.state == DEAD_FIRE_ACTION || this.state == DEAD_ACTION){
		drawEnemyScore(ctx,texture,oneUpTex,this.posX - scrollX,this.posY,this.score);
	}
}

大元はこちらです。

/**
 * 敵用のスコア描画処理
 * @param {*} ctx 
 * @param {*} texture 
 * @param {*} oneUpTex 
 * @param {*} posX 
 * @param {*} posY 
 * @param {*} score 
 */
function drawEnemyScore(ctx,texture,oneUpTex,posX,posY,score){
	// 8000点超えていれば1up
	if(score > 8000){
			ctx.drawImage(oneUpTex,416,480,64,32,(posX + 16) - (64 / 2),posY - 14,64,32);
	}
	else{
			var digits = getDigits(score);	// 桁数を取得
			var maxNumber = getMaxNumber(digits);
			// 3桁4桁で分ける
			var numberPosX = digits == 3 ? (posX + 16) - (18 / 2) - 18 : (posX + 16 - 9) - 28;
			// 全て描画するまで
			while(maxNumber >= 1){
				// 一番上の桁数から描画する
				g_Ctx.drawImage(gCoinTex, Math.floor((score / maxNumber )) * 20,0,20,17, numberPosX,posY - 4, 16, 13);
				score -= Math.floor((score / maxNumber)) * maxNumber;				// 一番上の桁数を引く(111だったら100を引く)
				maxNumber = Math.floor(maxNumber / 10);		// 111 = 11にする
				numberPosX += 18;
			}        
	}
}

スコアが8000点を越えていた場合は1upを描画するようにしています。

続いて、マリオがクリボを踏みつけた時の処理を書きます。

// マリオの下がクリボの中間地点よりも上にある
if(mario.posY + mario.height <= this.posY + (32 - this.height) + (this.height / 2)){
	// マリオが踏みつけた後の処理
	mario.stompAction();
	this.score = mario.getScore();
	// 潰れたアニメーションにする
	this.state = DEAD_ACTION;
	this.animY = 64;
	this.direction = LEFT_DIR;
	
}

マリオの踏みつけ回数によって、表示されるスコアが変わるので、
マリオから点数を受け取ってスコアを描画する
ようにしています。

では、続いて、マリオクラスの実装を行います。

mario.js

まず、メンバー変数に踏みつけ回数を記録するための変数を定義します。

// chapter47
// 連続踏みつけ数
this.sequenceJumpCnt = 0;

続いて、敵を踏みつけた後の処理を行う関数を定義します。

/**
 * chapter47
 * 踏みつけ後の処理
 * 上昇させる、スコアカウントを上げる
 */
Mario.prototype.stompAction = function(){
	this.jumpPower = STEP_UP_NUM;
	if(++this.sequenceJumpCnt >= STOMPING_SCORES.length - 1){
		this.sequenceJumpCnt = STOMPING_SCORES.length - 1;
		// one up
		this.playerNum++;
	}else{
		// 1upでなかったら、スコアに加算する
		gScore += this.getScore();
	}
}

踏みつけ回数が上限までいったら、そこでストップさせ、以降1upさせます

マリオが地面についたら、踏みつけ回数をリセットさせます。

// マリオの下側
if(isObjectMap(map[this.downMapY][this.rightMapX]) || isObjectMap(map[this.downMapY][this.leftMapX])){
	// (加算される前の)中心点からの距離を見る
	var centerY = this.height == 64 ? this.posY + 32 : this.posY;
	var vecY = Math.abs((centerY + HALF_MAP_SIZE) - ((this.downMapY * MAP_SIZE) + HALF_MAP_SIZE));
	// 地面についた
	// chapter47 地面についたら連続ジャンプのカウントをゼロにする
	this.sequenceJumpCnt = 0;
	return true;
}

あとは、キノコやコインを取得した際に、gScoreにスコアを加算させれば終了です。

続いて、ノコノコのスコア表示実装を行います。

ノコノコには、甲羅攻撃連続1up処理も実装する必要があります。

nokonoko.js

ノコノコは踏みつけられて死亡していない際にも点数の描画を行う必要があるので、
スコア描画フラグとスコア描画カウントを別に設けることにします。

// chapter47
this.score = 100;
this.isDrawScore = false;
this.scoreCnt = 0;
// 連続して甲羅を倒した数
this.sequenceAttackCnt = 0;

そして、スコア表示系の関数を定義します。

甲羅の連続攻撃処理もマリオの連続踏みつけの実装と変わりません。

/**
 * set collision reaction
 */
Noko.prototype.setDeadCollisionAction = function(){
	// スコアを表示させる
	this.setDrawScore();
	// fire用の死亡アニメーション
	this.state = DEAD_FIRE_ACTION;
	// 少しジャンプさせる
	this.addPosY = -8;
}

/**
 * スコアを描画する
 * @param {*} ctx 
 * @param {*} texture 
 * @param {*} oneUpTex 
 * @param {*} scrollX 
 */
Noko.prototype.drawScore = function(ctx,texture,oneUpTex,scrollX){
	if(this.isDrawScore && this.state != DEAD){
		drawEnemyScore(ctx,texture,oneUpTex,this.posX - scrollX,this.posY - 16,this.score);
	}
}

/**
 * スコアの表示時間を管理する
 */
Noko.prototype.updateScoreCnt = function(){
	if(this.isDrawScore){
		if(this.scoreCnt++ >= 120){
			this.isDrawScore = false;
		}
	}
}

/**
 * スコア描画フラグを立てる
 */
Noko.prototype.setDrawScore = function(){
	this.isDrawScore = true;
	this.scoreCnt = 0;
}

/**
 * 甲羅での連続アタック数を延ばす
 * 一定数を超えるとoneup
 */
Noko.prototype.increaseSequenceAttackCnt = function(mario){
	if(++this.sequenceAttackCnt >= STOMPING_SCORES.length - 1){
		this.sequenceAttackCnt = STOMPING_SCORES.length - 1;
		mario.playerNum++;
	}else{
		// 1upでなかったら、スコアに加算する
		gScore += this.getScore();
	}
}

/**
 * 連続倒しを加味したスコアを返す
 */
Noko.prototype.getScore = function(){
	return STOMPING_SCORES[this.sequenceAttackCnt];
}

甲羅で連続して敵を倒す時の処理(クリボ編)

問題の甲羅で連続して、敵を倒した時の実装を行います。

連続甲羅攻撃はマリオが甲羅を踏みつけると、0に解除され、
8000点を超えると1up処理が行われます。

// クリボ
if(kuribos != null){
	for(var i = 0;i < kuribos.length;++i){
		// 死亡チェック
		if(!kuribos[i].isDead()){
			// 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){
						// 連続ヒット数を伸ばす
						this.increaseSequenceAttackCnt(mario);
						kuribos[i].score = this.getScore();
						kuribos[i].setDeadCollisionAction();					
					}
				}
			}
		}
	}
}

前の実装で死亡チェックを忘れていて、多重判定になっていたので、忘れずに入れてください。

連続アタック回数を増加させ、連続アタック数を加味したスコアを代入、
スコアの描画
という形になります。

甲羅で連続して敵を倒す時の処理(ノコノコ編)

ノコノコは、ノコノコ同士が甲羅攻撃状態の場合は、両方に同じスコアを代入するようにします。

ノコノコ甲羅の相殺処理
if(nokos != null){
	for(var i = 0;i < nokos.length;++i){
		// 死亡チェック
		if(!nokos[i].isDead()){
			// ノコノコとの当たり判定があった場合に死亡状態になるので、死亡判定をする必要がある
			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){
								this.increaseSequenceAttackCnt(mario);
								// 相手ノコノコが攻撃状態の場合は自身も当たり判定を呼ぶ
								if(nokos[i].state == NOKO_ATTACK_STATE){
									this.score = this.getScore();
									this.setDeadCollisionAction();
								}
								nokos[i].score = this.getScore();
								nokos[i].setDeadCollisionAction();
							}
						}
					}
				}
			}
		}
	}		
}

ノコノコの踏みつけ処理

動画にあるように、ノコノコの踏みつけ時は甲羅で敵をアタック中のスコアとシンクロするので、踏みつけ時に連続アタック数を考慮し更新する必要があります。

// ノコノコが連続して倒している場合大きい方のスコアを代入する
if(mario.sequenceJumpCnt + 1 < this.sequenceAttackCnt){
	mario.sequenceJumpCnt = this.sequenceAttackCnt;
}
mario.stompAction();
this.score = mario.getScore();
// 甲羅を止めたら、連続カウントを0に戻す
this.sequenceAttackCnt = 0;
this.setDrawScore();
this.isSticky = true;

あとは、描画関数をDraw関数内で呼んで、大元の引数を変更すれば完成です。

/**
 * 
 * @param {*} ctx 
 * @param {*} texture 
 * @param {*} scoreTex 
 * @param {*} oneUpTex 
 * @param {*} scrollX 
 */
Noko.prototype.draw = function(ctx,texture,scoreTex,oneUpTex,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);
	// スコアの表示
	this.drawScore(ctx,scoreTex,oneUpTex,scrollX);
}

まとめ

全体はgithubを参照してください。

本当は、共通クラスを作って、そこにDrawScore関数を定義したかったんですが、結構変更しないといけないため、共通関数を用意する形にしました。

あとは、キノコを取った時などに、確かスコアを表示していましたが、
そこまで、有用性がないのでカットしました。

次は、キノコ取得時の当たり判定をやろうかなと思ってます。